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 +12 -30
- package/package.json +1 -1
- package/packages/web/app/page.tsx +6 -1
- package/packages/web/components/AppSidebar/AppSidebar.tsx +5 -3
- package/packages/web/components/MigrationGate/MigrationGate.tsx +304 -0
- package/packages/web/components/MigrationGate/index.ts +1 -0
- package/packages/web/lib/services/migration.server.ts +4 -24
package/bin/serve.js
CHANGED
|
@@ -65,55 +65,39 @@ function writeState(state) {
|
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
/**
|
|
68
|
-
* Get package.json version
|
|
68
|
+
* Get package.json version
|
|
69
69
|
*/
|
|
70
|
-
function
|
|
70
|
+
function getPackageVersion(pkgPath) {
|
|
71
71
|
try {
|
|
72
72
|
const pkg = JSON.parse(fs.readFileSync(path.join(pkgPath, 'package.json'), 'utf8'))
|
|
73
|
-
|
|
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
|
|
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,
|
|
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
|
|
100
|
-
const
|
|
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 (
|
|
109
|
-
return { needed: true, reason:
|
|
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:
|
|
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,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
|
|
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
|
-
|
|
41
|
-
<
|
|
42
|
-
|
|
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
|
-
//
|
|
477
|
-
//
|
|
478
|
-
|
|
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 = {
|