prjct-cli 0.12.1 → 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.1",
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
  }
@@ -37,9 +37,11 @@ function SidebarContent({
37
37
  "flex h-14 items-center border-b border-border",
38
38
  isCollapsed ? "justify-center px-2" : "justify-between px-3"
39
39
  )}>
40
- <Link href="/" onClick={onNavigate}>
41
- <Logo size="xs" showText={!isCollapsed} rounded />
42
- </Link>
40
+ {!isCollapsed && (
41
+ <Link href="/" onClick={onNavigate}>
42
+ <Logo size="xs" showText rounded />
43
+ </Link>
44
+ )}
43
45
  {onToggleCollapse && (
44
46
  <Tooltip>
45
47
  <TooltipTrigger asChild>
@@ -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,27 +469,11 @@ DESCRIPTION EXTRACTION:
473
469
  deletedFiles = await deleteLegacyFiles(projectId)
474
470
  }
475
471
 
476
- // Generate views from new JSON files
477
- // Fire and forget - don't block request to prevent OOM from subprocess spawning
478
- let viewsGenerated = false
479
- if (allSuccess) {
480
- try {
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
484
- viewsGenerated = true
485
- results.push({ file: 'views', success: true })
486
- } catch (viewError) {
487
- // Views generation failed but migration still succeeded
488
- results.push({
489
- file: 'views',
490
- success: false,
491
- error: viewError instanceof Error ? viewError.message : 'Failed to generate views'
492
- })
493
- }
494
- }
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
495
475
 
496
- return { success: allSuccess, results, deletedFiles, viewsGenerated }
476
+ return { success: allSuccess, results, deletedFiles, viewsGenerated: false }
497
477
  }
498
478
 
499
479
  export type ProjectInfo = {