prjct-cli 0.12.0 → 0.12.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.
package/bin/serve.js CHANGED
@@ -65,55 +65,39 @@ function writeState(state) {
65
65
  }
66
66
 
67
67
  /**
68
- * Get package.json version and hash of dependencies
68
+ * Get package.json version
69
69
  */
70
- function getPackageInfo(pkgPath) {
70
+ function getPackageVersion(pkgPath) {
71
71
  try {
72
72
  const pkg = JSON.parse(fs.readFileSync(path.join(pkgPath, 'package.json'), 'utf8'))
73
- const deps = JSON.stringify(pkg.dependencies || {}) + JSON.stringify(pkg.devDependencies || {})
74
- // Simple hash of dependencies
75
- let hash = 0
76
- for (let i = 0; i < deps.length; i++) {
77
- const char = deps.charCodeAt(i)
78
- hash = ((hash << 5) - hash) + char
79
- hash = hash & hash // Convert to 32bit integer
80
- }
81
- return { version: pkg.version, depsHash: hash.toString(16) }
73
+ return pkg.version || '0.0.0'
82
74
  } catch {
83
- return { version: '0.0.0', depsHash: '0' }
75
+ return '0.0.0'
84
76
  }
85
77
  }
86
78
 
87
79
  /**
88
80
  * Check if dependencies need to be installed
81
+ * Simple logic: install only if node_modules missing OR version changed
89
82
  */
90
83
  function needsInstall(pkgDir, stateKey) {
91
84
  const nodeModules = path.join(pkgDir, 'node_modules')
92
85
 
93
- // If node_modules doesn't exist, definitely need install
86
+ // If node_modules doesn't exist, need install
94
87
  if (!fs.existsSync(nodeModules)) {
95
88
  return { needed: true, reason: 'node_modules not found' }
96
89
  }
97
90
 
98
91
  const state = readState()
99
- const pkgInfo = getPackageInfo(pkgDir)
100
- const savedInfo = state[stateKey]
101
-
102
- // If no saved state, need install to track
103
- if (!savedInfo) {
104
- return { needed: true, reason: 'first time tracking' }
105
- }
92
+ const currentVersion = getPackageVersion(pkgDir)
93
+ const savedVersion = state[stateKey]?.version
106
94
 
107
95
  // If version changed, need install
108
- if (savedInfo.version !== pkgInfo.version) {
109
- return { needed: true, reason: `version changed: ${savedInfo.version} → ${pkgInfo.version}` }
110
- }
111
-
112
- // If dependencies hash changed, need install
113
- if (savedInfo.depsHash !== pkgInfo.depsHash) {
114
- return { needed: true, reason: 'dependencies changed' }
96
+ if (savedVersion && savedVersion !== currentVersion) {
97
+ return { needed: true, reason: `${savedVersion} → ${currentVersion}` }
115
98
  }
116
99
 
100
+ // node_modules exists and version unchanged = skip
117
101
  return { needed: false }
118
102
  }
119
103
 
@@ -122,10 +106,8 @@ function needsInstall(pkgDir, stateKey) {
122
106
  */
123
107
  function markInstalled(pkgDir, stateKey) {
124
108
  const state = readState()
125
- const pkgInfo = getPackageInfo(pkgDir)
126
109
  state[stateKey] = {
127
- version: pkgInfo.version,
128
- depsHash: pkgInfo.depsHash,
110
+ version: getPackageVersion(pkgDir),
129
111
  installedAt: new Date().toISOString()
130
112
  }
131
113
  writeState(state)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prjct-cli",
3
- "version": "0.12.0",
3
+ "version": "0.12.2",
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": {
@@ -1,6 +1,7 @@
1
1
  import { getProjects } from '@/lib/services/projects.server'
2
2
  import { getGlobalStats } from '@/lib/services/stats.server'
3
3
  import { DashboardContent } from '@/components/DashboardContent'
4
+ import { MigrationGate } from '@/components/MigrationGate'
4
5
 
5
6
  export default async function Dashboard() {
6
7
  const [projects, stats] = await Promise.all([
@@ -8,5 +9,9 @@ export default async function Dashboard() {
8
9
  getGlobalStats()
9
10
  ])
10
11
 
11
- return <DashboardContent projects={projects} stats={stats} />
12
+ return (
13
+ <MigrationGate>
14
+ <DashboardContent projects={projects} stats={stats} />
15
+ </MigrationGate>
16
+ )
12
17
  }
@@ -7,81 +7,132 @@ 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">
27
- <Link href="/" onClick={onNavigate}>
28
- <Logo size="xs" showText rounded />
29
- </Link>
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
+ )}>
40
+ {!isCollapsed && (
41
+ <Link href="/" onClick={onNavigate}>
42
+ <Logo size="xs" showText rounded />
43
+ </Link>
44
+ )}
45
+ {onToggleCollapse && (
46
+ <Tooltip>
47
+ <TooltipTrigger asChild>
48
+ <button
49
+ onClick={onToggleCollapse}
50
+ className="h-8 w-8 rounded-md flex items-center justify-center text-muted-foreground hover:bg-accent hover:text-accent-foreground transition-colors"
51
+ aria-label={isCollapsed ? "Expand sidebar" : "Collapse sidebar"}
52
+ >
53
+ <PanelLeft className="h-4 w-4" />
54
+ </button>
55
+ </TooltipTrigger>
56
+ <TooltipContent side="right">
57
+ {isCollapsed ? "Expand" : "Collapse"}
58
+ </TooltipContent>
59
+ </Tooltip>
60
+ )}
30
61
  </div>
31
62
 
32
63
  {/* Nav */}
33
- <nav className="flex-1 overflow-y-auto py-4 px-3">
64
+ <nav className={cn("flex-1 overflow-y-auto py-4", isCollapsed ? "px-2" : "px-3")}>
34
65
  <div className="space-y-1">
35
66
  {navItems.map(({ href, icon: Icon, label }) => {
36
67
  const isActive = pathname === href || (href !== '/' && pathname.startsWith(href))
37
- return (
68
+ const linkContent = (
38
69
  <Link
39
70
  key={href}
40
71
  href={href}
41
72
  onClick={onNavigate}
42
73
  className={cn(
43
- 'flex items-center gap-3 rounded-md px-3 py-2.5 transition-colors min-h-[44px]',
74
+ 'flex items-center rounded-md transition-colors min-h-[44px]',
75
+ isCollapsed ? 'justify-center px-2' : 'gap-3 px-3',
76
+ 'py-2.5',
44
77
  isActive
45
78
  ? 'bg-accent text-accent-foreground'
46
79
  : 'text-muted-foreground hover:bg-accent hover:text-accent-foreground'
47
80
  )}
48
81
  >
49
82
  <Icon className="h-5 w-5 shrink-0" />
50
- <span className="text-sm font-medium">{label}</span>
83
+ {!isCollapsed && <span className="text-sm font-medium">{label}</span>}
51
84
  </Link>
52
85
  )
86
+
87
+ if (isCollapsed) {
88
+ return (
89
+ <Tooltip key={href}>
90
+ <TooltipTrigger asChild>{linkContent}</TooltipTrigger>
91
+ <TooltipContent side="right">{label}</TooltipContent>
92
+ </Tooltip>
93
+ )
94
+ }
95
+ return linkContent
53
96
  })}
54
97
  </div>
55
98
  </nav>
56
99
 
57
100
  {/* 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>
101
+ <div className={cn("border-t border-border space-y-1", isCollapsed ? "p-2" : "p-3")}>
102
+ {[
103
+ { href: '/settings', icon: Settings, label: 'Settings' },
104
+ { href: '/help', icon: HelpCircle, label: 'Need help?' }
105
+ ].map(({ href, icon: Icon, label }) => {
106
+ const isActive = pathname === href
107
+ const linkContent = (
108
+ <Link
109
+ key={href}
110
+ href={href}
111
+ onClick={onNavigate}
112
+ className={cn(
113
+ 'flex items-center rounded-md transition-colors min-h-[44px]',
114
+ isCollapsed ? 'justify-center px-2' : 'gap-3 px-3',
115
+ 'py-2.5',
116
+ isActive
117
+ ? 'bg-accent text-accent-foreground'
118
+ : 'text-muted-foreground hover:bg-accent hover:text-accent-foreground'
119
+ )}
120
+ >
121
+ <Icon className="h-5 w-5 shrink-0" />
122
+ {!isCollapsed && <span className="text-sm font-medium">{label}</span>}
123
+ </Link>
124
+ )
125
+
126
+ if (isCollapsed) {
127
+ return (
128
+ <Tooltip key={href}>
129
+ <TooltipTrigger asChild>{linkContent}</TooltipTrigger>
130
+ <TooltipContent side="right">{label}</TooltipContent>
131
+ </Tooltip>
132
+ )
133
+ }
134
+ return linkContent
135
+ })}
85
136
  </div>
86
137
  </>
87
138
  )
@@ -90,6 +141,7 @@ function SidebarContent({ onNavigate }: { onNavigate?: () => void }) {
90
141
  export function AppSidebar() {
91
142
  const [isOpen, setIsOpen] = useState(false)
92
143
  const [isMobile, setIsMobile] = useState(false)
144
+ const [isCollapsed, setIsCollapsed] = useState(false)
93
145
 
94
146
  // Detect mobile on mount and resize
95
147
  useEffect(() => {
@@ -120,10 +172,16 @@ export function AppSidebar() {
120
172
  )
121
173
  }
122
174
 
123
- // Desktop: Static sidebar
175
+ // Desktop: Collapsible sidebar
124
176
  return (
125
- <aside className="hidden md:flex h-full w-60 flex-col border-r border-border bg-card">
126
- <SidebarContent />
177
+ <aside className={cn(
178
+ "hidden md:flex h-full flex-col border-r border-border bg-card transition-all duration-200",
179
+ isCollapsed ? "w-14" : "w-60"
180
+ )}>
181
+ <SidebarContent
182
+ isCollapsed={isCollapsed}
183
+ onToggleCollapse={() => setIsCollapsed(!isCollapsed)}
184
+ />
127
185
  </aside>
128
186
  )
129
187
  }
@@ -0,0 +1,304 @@
1
+ 'use client'
2
+
3
+ import { useState, useEffect } from 'react'
4
+ import { Button } from '@/components/ui/button'
5
+ import { Input } from '@/components/ui/input'
6
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
7
+ import { CheckCircle2, Circle, Loader2, Key, Package, ExternalLink } from 'lucide-react'
8
+ import { cn } from '@/lib/utils'
9
+
10
+ interface ProjectInfo {
11
+ id: string
12
+ name: string
13
+ needsMigration: boolean
14
+ }
15
+
16
+ interface MigrationResult {
17
+ file: string
18
+ success: boolean
19
+ error?: string
20
+ }
21
+
22
+ type Status = 'checking' | 'needs-key' | 'needs-migration' | 'migrating' | 'ready'
23
+
24
+ interface MigrationGateProps {
25
+ children: React.ReactNode
26
+ }
27
+
28
+ export function MigrationGate({ children }: MigrationGateProps) {
29
+ const [status, setStatus] = useState<Status>('checking')
30
+ const [projects, setProjects] = useState<ProjectInfo[]>([])
31
+ const [apiKey, setApiKey] = useState('')
32
+ const [savingKey, setSavingKey] = useState(false)
33
+ const [migratingProject, setMigratingProject] = useState<string | null>(null)
34
+ const [migrationResults, setMigrationResults] = useState<Record<string, MigrationResult[]>>({})
35
+ const [error, setError] = useState<string | null>(null)
36
+
37
+ useEffect(() => {
38
+ checkStatus()
39
+ }, [])
40
+
41
+ async function checkStatus() {
42
+ try {
43
+ // 1. Check API key
44
+ const settingsRes = await fetch('/api/settings')
45
+ const settings = await settingsRes.json()
46
+
47
+ if (!settings.data?.hasApiKey) {
48
+ setStatus('needs-key')
49
+ return
50
+ }
51
+
52
+ // 2. Check projects needing migration
53
+ const migrateRes = await fetch('/api/migrate')
54
+ const migrate = await migrateRes.json()
55
+
56
+ if (migrate.data?.projects?.length > 0) {
57
+ setProjects(migrate.data.projects.map((p: { id: string; name: string }) => ({
58
+ ...p,
59
+ needsMigration: true
60
+ })))
61
+ setStatus('needs-migration')
62
+ return
63
+ }
64
+
65
+ setStatus('ready')
66
+ } catch (err) {
67
+ console.error('Error checking migration status:', err)
68
+ // On error, just show the dashboard
69
+ setStatus('ready')
70
+ }
71
+ }
72
+
73
+ async function saveApiKey() {
74
+ if (!apiKey.trim()) return
75
+
76
+ setSavingKey(true)
77
+ setError(null)
78
+
79
+ try {
80
+ const res = await fetch('/api/settings', {
81
+ method: 'POST',
82
+ headers: { 'Content-Type': 'application/json' },
83
+ body: JSON.stringify({ openRouterApiKey: apiKey.trim() })
84
+ })
85
+
86
+ const data = await res.json()
87
+
88
+ if (data.success) {
89
+ setApiKey('')
90
+ await checkStatus() // Re-check, might need migration now
91
+ } else {
92
+ setError(data.error || 'Failed to save API key')
93
+ }
94
+ } catch (err) {
95
+ setError('Failed to save API key')
96
+ } finally {
97
+ setSavingKey(false)
98
+ }
99
+ }
100
+
101
+ async function migrateProject(projectId: string) {
102
+ setMigratingProject(projectId)
103
+ setError(null)
104
+
105
+ try {
106
+ const res = await fetch('/api/migrate', {
107
+ method: 'POST',
108
+ headers: { 'Content-Type': 'application/json' },
109
+ body: JSON.stringify({ projectId })
110
+ })
111
+
112
+ const data = await res.json()
113
+
114
+ if (data.success && data.data?.success) {
115
+ // Mark project as migrated
116
+ setProjects(prev => prev.map(p =>
117
+ p.id === projectId ? { ...p, needsMigration: false } : p
118
+ ))
119
+ setMigrationResults(prev => ({
120
+ ...prev,
121
+ [projectId]: data.data.results
122
+ }))
123
+
124
+ // Check if all done
125
+ const remaining = projects.filter(p => p.id !== projectId && p.needsMigration)
126
+ if (remaining.length === 0) {
127
+ setTimeout(() => setStatus('ready'), 1000)
128
+ }
129
+ } else {
130
+ setError(data.error || data.data?.error || 'Migration failed')
131
+ setMigrationResults(prev => ({
132
+ ...prev,
133
+ [projectId]: data.data?.results || []
134
+ }))
135
+ }
136
+ } catch (err) {
137
+ setError('Migration request failed')
138
+ } finally {
139
+ setMigratingProject(null)
140
+ }
141
+ }
142
+
143
+ async function migrateAll() {
144
+ const toMigrate = projects.filter(p => p.needsMigration)
145
+ for (const project of toMigrate) {
146
+ await migrateProject(project.id)
147
+ }
148
+ }
149
+
150
+ // Ready - show dashboard
151
+ if (status === 'ready') {
152
+ return <>{children}</>
153
+ }
154
+
155
+ // Loading
156
+ if (status === 'checking') {
157
+ return (
158
+ <div className="flex items-center justify-center min-h-[60vh]">
159
+ <div className="text-center space-y-4">
160
+ <Loader2 className="h-8 w-8 animate-spin mx-auto text-muted-foreground" />
161
+ <p className="text-muted-foreground">Checking migration status...</p>
162
+ </div>
163
+ </div>
164
+ )
165
+ }
166
+
167
+ // Needs API Key
168
+ if (status === 'needs-key') {
169
+ return (
170
+ <div className="flex items-center justify-center min-h-[60vh] p-4">
171
+ <Card className="w-full max-w-md">
172
+ <CardHeader className="text-center">
173
+ <div className="mx-auto mb-4 h-12 w-12 rounded-full bg-primary/10 flex items-center justify-center">
174
+ <Key className="h-6 w-6 text-primary" />
175
+ </div>
176
+ <CardTitle>OpenRouter API Key Required</CardTitle>
177
+ <CardDescription>
178
+ prjct needs an OpenRouter API key to migrate your projects to the new JSON format.
179
+ </CardDescription>
180
+ </CardHeader>
181
+ <CardContent className="space-y-4">
182
+ <div className="space-y-2">
183
+ <Input
184
+ type="password"
185
+ placeholder="sk-or-v1-..."
186
+ value={apiKey}
187
+ onChange={(e) => setApiKey(e.target.value)}
188
+ onKeyDown={(e) => e.key === 'Enter' && saveApiKey()}
189
+ />
190
+ <a
191
+ href="https://openrouter.ai/keys"
192
+ target="_blank"
193
+ rel="noopener noreferrer"
194
+ className="text-sm text-muted-foreground hover:text-primary flex items-center gap-1"
195
+ >
196
+ Get your key at openrouter.ai/keys
197
+ <ExternalLink className="h-3 w-3" />
198
+ </a>
199
+ </div>
200
+
201
+ {error && (
202
+ <p className="text-sm text-destructive">{error}</p>
203
+ )}
204
+
205
+ <Button
206
+ onClick={saveApiKey}
207
+ disabled={!apiKey.trim() || savingKey}
208
+ className="w-full"
209
+ >
210
+ {savingKey ? (
211
+ <>
212
+ <Loader2 className="h-4 w-4 mr-2 animate-spin" />
213
+ Saving...
214
+ </>
215
+ ) : (
216
+ 'Save Key'
217
+ )}
218
+ </Button>
219
+ </CardContent>
220
+ </Card>
221
+ </div>
222
+ )
223
+ }
224
+
225
+ // Needs Migration
226
+ return (
227
+ <div className="flex items-center justify-center min-h-[60vh] p-4">
228
+ <Card className="w-full max-w-lg">
229
+ <CardHeader className="text-center">
230
+ <div className="mx-auto mb-4 h-12 w-12 rounded-full bg-primary/10 flex items-center justify-center">
231
+ <Package className="h-6 w-6 text-primary" />
232
+ </div>
233
+ <CardTitle>Migration Required</CardTitle>
234
+ <CardDescription>
235
+ The following projects need to be migrated to the new JSON format.
236
+ </CardDescription>
237
+ </CardHeader>
238
+ <CardContent className="space-y-4">
239
+ <div className="border rounded-lg divide-y">
240
+ {projects.map((project) => {
241
+ const isMigrating = migratingProject === project.id
242
+ const results = migrationResults[project.id]
243
+ const isMigrated = !project.needsMigration
244
+
245
+ return (
246
+ <div key={project.id} className="p-3 flex items-center justify-between gap-3">
247
+ <div className="flex items-center gap-3 min-w-0">
248
+ {isMigrated ? (
249
+ <CheckCircle2 className="h-5 w-5 text-green-500 shrink-0" />
250
+ ) : isMigrating ? (
251
+ <Loader2 className="h-5 w-5 animate-spin text-primary shrink-0" />
252
+ ) : (
253
+ <Circle className="h-5 w-5 text-muted-foreground shrink-0" />
254
+ )}
255
+ <span className={cn(
256
+ "truncate",
257
+ isMigrated && "text-muted-foreground"
258
+ )}>
259
+ {project.name}
260
+ </span>
261
+ </div>
262
+
263
+ {isMigrated ? (
264
+ <span className="text-sm text-green-600">Migrated</span>
265
+ ) : (
266
+ <Button
267
+ size="sm"
268
+ variant="outline"
269
+ onClick={() => migrateProject(project.id)}
270
+ disabled={!!migratingProject}
271
+ >
272
+ {isMigrating ? 'Migrating...' : 'Migrate'}
273
+ </Button>
274
+ )}
275
+ </div>
276
+ )
277
+ })}
278
+ </div>
279
+
280
+ {error && (
281
+ <p className="text-sm text-destructive">{error}</p>
282
+ )}
283
+
284
+ {projects.some(p => p.needsMigration) && (
285
+ <Button
286
+ onClick={migrateAll}
287
+ disabled={!!migratingProject}
288
+ className="w-full"
289
+ >
290
+ {migratingProject ? (
291
+ <>
292
+ <Loader2 className="h-4 w-4 mr-2 animate-spin" />
293
+ Migrating...
294
+ </>
295
+ ) : (
296
+ 'Migrate All'
297
+ )}
298
+ </Button>
299
+ )}
300
+ </CardContent>
301
+ </Card>
302
+ </div>
303
+ )
304
+ }
@@ -0,0 +1 @@
1
+ export { MigrationGate } from './MigrationGate'
@@ -3,13 +3,9 @@ import { generateText } from 'ai'
3
3
  import { promises as fs } from 'fs'
4
4
  import { join } from 'path'
5
5
  import { homedir } from 'os'
6
- import { exec } from 'child_process'
7
- import { promisify } from 'util'
8
6
 
9
- const execAsync = promisify(exec)
10
7
  const SETTINGS_PATH = join(homedir(), '.prjct-cli', 'settings.json')
11
8
  const GLOBAL_STORAGE = join(homedir(), '.prjct-cli', 'projects')
12
- const PRJCT_CLI_PATH = join(__dirname, '..', '..', '..', '..')
13
9
 
14
10
  // Complete JSON Schema definitions for new architecture
15
11
  // JSON is source of truth, MD is generated for Claude
@@ -473,25 +469,11 @@ DESCRIPTION EXTRACTION:
473
469
  deletedFiles = await deleteLegacyFiles(projectId)
474
470
  }
475
471
 
476
- // Generate views from new JSON files
477
- let viewsGenerated = false
478
- if (allSuccess) {
479
- try {
480
- // Try to run the generate-views CLI command
481
- await execAsync(`bun ${join(PRJCT_CLI_PATH, 'bin', 'generate-views.js')} --project=${projectId}`)
482
- viewsGenerated = true
483
- results.push({ file: 'views', success: true })
484
- } catch (viewError) {
485
- // Views generation failed but migration still succeeded
486
- results.push({
487
- file: 'views',
488
- success: false,
489
- error: viewError instanceof Error ? viewError.message : 'Failed to generate views'
490
- })
491
- }
492
- }
472
+ // NOTE: View generation removed from migration to prevent Bun crashes
473
+ // Views are generated on-demand by the view-generator when needed
474
+ // The JSON files in data/ are the source of truth now
493
475
 
494
- return { success: allSuccess, results, deletedFiles, viewsGenerated }
476
+ return { success: allSuccess, results, deletedFiles, viewsGenerated: false }
495
477
  }
496
478
 
497
479
  export type ProjectInfo = {
@@ -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