prjct-cli 0.12.2 → 0.13.0
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/CHANGELOG.md +43 -0
- package/CLAUDE.md +18 -6
- package/core/data/index.ts +19 -5
- package/core/data/md-base-manager.ts +203 -0
- package/core/data/md-queue-manager.ts +179 -0
- package/core/data/md-state-manager.ts +133 -0
- package/core/serializers/index.ts +20 -0
- package/core/serializers/queue-serializer.ts +210 -0
- package/core/serializers/state-serializer.ts +136 -0
- package/core/utils/file-helper.ts +12 -0
- package/package.json +1 -1
- package/packages/web/app/api/projects/[id]/stats/route.ts +6 -29
- package/packages/web/app/page.tsx +1 -6
- package/packages/web/app/project/[id]/page.tsx +34 -1
- package/packages/web/app/project/[id]/stats/page.tsx +11 -5
- package/packages/web/app/settings/page.tsx +2 -221
- package/packages/web/components/BlockersCard/BlockersCard.tsx +67 -0
- package/packages/web/components/BlockersCard/BlockersCard.types.ts +11 -0
- package/packages/web/components/BlockersCard/index.ts +2 -0
- package/packages/web/components/CommandButton/CommandButton.tsx +10 -3
- package/packages/web/lib/projects.ts +28 -27
- package/packages/web/lib/services/projects.server.ts +25 -21
- package/packages/web/lib/services/stats.server.ts +355 -57
- package/packages/web/package.json +0 -2
- package/templates/commands/decision.md +226 -0
- package/templates/commands/done.md +100 -68
- package/templates/commands/feature.md +102 -103
- package/templates/commands/idea.md +41 -38
- package/templates/commands/now.md +94 -33
- package/templates/commands/pause.md +90 -30
- package/templates/commands/ship.md +179 -74
- package/templates/commands/sync.md +324 -200
- package/packages/web/app/api/migrate/route.ts +0 -46
- package/packages/web/app/api/settings/route.ts +0 -97
- package/packages/web/app/api/v2/projects/[id]/unified/route.ts +0 -57
- package/packages/web/components/MigrationGate/MigrationGate.tsx +0 -304
- package/packages/web/components/MigrationGate/index.ts +0 -1
- package/packages/web/lib/json-loader.ts +0 -630
- package/packages/web/lib/services/migration.server.ts +0 -580
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
import { NextResponse } from 'next/server'
|
|
2
|
-
import { migrateProject, getProjectsToMigrate } from '@/lib/services/migration.server'
|
|
3
|
-
|
|
4
|
-
export async function GET() {
|
|
5
|
-
try {
|
|
6
|
-
const projects = await getProjectsToMigrate()
|
|
7
|
-
return NextResponse.json({
|
|
8
|
-
success: true,
|
|
9
|
-
data: { projects }
|
|
10
|
-
})
|
|
11
|
-
} catch (error) {
|
|
12
|
-
return NextResponse.json(
|
|
13
|
-
{ success: false, error: 'Failed to list projects' },
|
|
14
|
-
{ status: 500 }
|
|
15
|
-
)
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export async function POST(request: Request) {
|
|
20
|
-
try {
|
|
21
|
-
const body = await request.json()
|
|
22
|
-
const { projectId } = body
|
|
23
|
-
|
|
24
|
-
if (!projectId) {
|
|
25
|
-
return NextResponse.json(
|
|
26
|
-
{ success: false, error: 'Project ID is required' },
|
|
27
|
-
{ status: 400 }
|
|
28
|
-
)
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
const result = await migrateProject(projectId)
|
|
32
|
-
|
|
33
|
-
return NextResponse.json({
|
|
34
|
-
success: result.success,
|
|
35
|
-
data: result
|
|
36
|
-
})
|
|
37
|
-
} catch (error) {
|
|
38
|
-
return NextResponse.json(
|
|
39
|
-
{
|
|
40
|
-
success: false,
|
|
41
|
-
error: error instanceof Error ? error.message : 'Migration failed'
|
|
42
|
-
},
|
|
43
|
-
{ status: 500 }
|
|
44
|
-
)
|
|
45
|
-
}
|
|
46
|
-
}
|
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
import { NextResponse } from 'next/server'
|
|
2
|
-
import { promises as fs } from 'fs'
|
|
3
|
-
import { join } from 'path'
|
|
4
|
-
import { homedir } from 'os'
|
|
5
|
-
|
|
6
|
-
const SETTINGS_PATH = join(homedir(), '.prjct-cli', 'settings.json')
|
|
7
|
-
|
|
8
|
-
interface Settings {
|
|
9
|
-
openRouterApiKey?: string
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
async function getSettings(): Promise<Settings> {
|
|
13
|
-
try {
|
|
14
|
-
const content = await fs.readFile(SETTINGS_PATH, 'utf-8')
|
|
15
|
-
return JSON.parse(content)
|
|
16
|
-
} catch {
|
|
17
|
-
return {}
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
async function saveSettings(settings: Settings): Promise<void> {
|
|
22
|
-
const dir = join(homedir(), '.prjct-cli')
|
|
23
|
-
await fs.mkdir(dir, { recursive: true })
|
|
24
|
-
await fs.writeFile(SETTINGS_PATH, JSON.stringify(settings, null, 2))
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export async function GET() {
|
|
28
|
-
try {
|
|
29
|
-
const settings = await getSettings()
|
|
30
|
-
// Mask the API key for security (only show last 4 chars)
|
|
31
|
-
const maskedKey = settings.openRouterApiKey
|
|
32
|
-
? `sk-...${settings.openRouterApiKey.slice(-4)}`
|
|
33
|
-
: null
|
|
34
|
-
|
|
35
|
-
return NextResponse.json({
|
|
36
|
-
success: true,
|
|
37
|
-
data: {
|
|
38
|
-
hasApiKey: !!settings.openRouterApiKey,
|
|
39
|
-
maskedKey
|
|
40
|
-
}
|
|
41
|
-
})
|
|
42
|
-
} catch (error) {
|
|
43
|
-
return NextResponse.json(
|
|
44
|
-
{ success: false, error: 'Failed to read settings' },
|
|
45
|
-
{ status: 500 }
|
|
46
|
-
)
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export async function POST(request: Request) {
|
|
51
|
-
try {
|
|
52
|
-
const body = await request.json()
|
|
53
|
-
const { openRouterApiKey } = body
|
|
54
|
-
|
|
55
|
-
if (!openRouterApiKey || typeof openRouterApiKey !== 'string') {
|
|
56
|
-
return NextResponse.json(
|
|
57
|
-
{ success: false, error: 'Invalid API key' },
|
|
58
|
-
{ status: 400 }
|
|
59
|
-
)
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
const settings = await getSettings()
|
|
63
|
-
settings.openRouterApiKey = openRouterApiKey
|
|
64
|
-
await saveSettings(settings)
|
|
65
|
-
|
|
66
|
-
return NextResponse.json({
|
|
67
|
-
success: true,
|
|
68
|
-
data: {
|
|
69
|
-
hasApiKey: true,
|
|
70
|
-
maskedKey: `sk-...${openRouterApiKey.slice(-4)}`
|
|
71
|
-
}
|
|
72
|
-
})
|
|
73
|
-
} catch (error) {
|
|
74
|
-
return NextResponse.json(
|
|
75
|
-
{ success: false, error: 'Failed to save settings' },
|
|
76
|
-
{ status: 500 }
|
|
77
|
-
)
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
export async function DELETE() {
|
|
82
|
-
try {
|
|
83
|
-
const settings = await getSettings()
|
|
84
|
-
delete settings.openRouterApiKey
|
|
85
|
-
await saveSettings(settings)
|
|
86
|
-
|
|
87
|
-
return NextResponse.json({
|
|
88
|
-
success: true,
|
|
89
|
-
data: { hasApiKey: false, maskedKey: null }
|
|
90
|
-
})
|
|
91
|
-
} catch (error) {
|
|
92
|
-
return NextResponse.json(
|
|
93
|
-
{ success: false, error: 'Failed to delete API key' },
|
|
94
|
-
{ status: 500 }
|
|
95
|
-
)
|
|
96
|
-
}
|
|
97
|
-
}
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
import { NextRequest, NextResponse } from 'next/server'
|
|
2
|
-
import { loadUnifiedJsonData } from '@/lib/json-loader'
|
|
3
|
-
|
|
4
|
-
export async function GET(
|
|
5
|
-
request: NextRequest,
|
|
6
|
-
{ params }: { params: Promise<{ id: string }> }
|
|
7
|
-
) {
|
|
8
|
-
const { id: projectId } = await params
|
|
9
|
-
|
|
10
|
-
if (!projectId) {
|
|
11
|
-
return NextResponse.json(
|
|
12
|
-
{ success: false, error: 'Project ID required' },
|
|
13
|
-
{ status: 400 }
|
|
14
|
-
)
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
try {
|
|
18
|
-
// Load all JSON data directly
|
|
19
|
-
const jsonData = await loadUnifiedJsonData(projectId)
|
|
20
|
-
|
|
21
|
-
if (!jsonData.hasJsonData) {
|
|
22
|
-
// No JSON files exist yet
|
|
23
|
-
return NextResponse.json({
|
|
24
|
-
success: true,
|
|
25
|
-
version: 'v2-empty',
|
|
26
|
-
state: null,
|
|
27
|
-
project: null,
|
|
28
|
-
agents: [],
|
|
29
|
-
ideas: [],
|
|
30
|
-
roadmap: [],
|
|
31
|
-
shipped: [],
|
|
32
|
-
analysis: null,
|
|
33
|
-
outcomes: [],
|
|
34
|
-
insights: {
|
|
35
|
-
healthScore: 0,
|
|
36
|
-
estimateAccuracy: 0,
|
|
37
|
-
topBlockers: [],
|
|
38
|
-
patternsDetected: [],
|
|
39
|
-
recommendations: ['Run /p:sync to initialize project data'],
|
|
40
|
-
},
|
|
41
|
-
hasJsonData: false,
|
|
42
|
-
})
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
return NextResponse.json({
|
|
46
|
-
success: true,
|
|
47
|
-
version: 'v2',
|
|
48
|
-
...jsonData,
|
|
49
|
-
})
|
|
50
|
-
} catch (error) {
|
|
51
|
-
console.error('[API v2] Error getting unified project data:', error)
|
|
52
|
-
return NextResponse.json(
|
|
53
|
-
{ success: false, error: 'Failed to get project data' },
|
|
54
|
-
{ status: 500 }
|
|
55
|
-
)
|
|
56
|
-
}
|
|
57
|
-
}
|
|
@@ -1,304 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { MigrationGate } from './MigrationGate'
|