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,17 +1,12 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
|
3
|
+
import { useQuery } from '@tanstack/react-query'
|
|
5
4
|
import { useTheme } from 'next-themes'
|
|
6
|
-
import { Settings as SettingsIcon, CheckCircle, XCircle, Terminal, Sun, Moon, Monitor
|
|
5
|
+
import { Settings as SettingsIcon, CheckCircle, XCircle, Terminal, Sun, Moon, Monitor } from 'lucide-react'
|
|
7
6
|
import { Button } from '@/components/ui/button'
|
|
8
|
-
import { Input } from '@/components/ui/input'
|
|
9
7
|
|
|
10
8
|
export default function Settings() {
|
|
11
9
|
const { theme, setTheme } = useTheme()
|
|
12
|
-
const queryClient = useQueryClient()
|
|
13
|
-
const [apiKey, setApiKey] = useState('')
|
|
14
|
-
const [showKey, setShowKey] = useState(false)
|
|
15
10
|
|
|
16
11
|
const { data: claudeStatus } = useQuery({
|
|
17
12
|
queryKey: ['claude-status'],
|
|
@@ -22,79 +17,6 @@ export default function Settings() {
|
|
|
22
17
|
}
|
|
23
18
|
})
|
|
24
19
|
|
|
25
|
-
const { data: settings } = useQuery({
|
|
26
|
-
queryKey: ['settings'],
|
|
27
|
-
queryFn: async () => {
|
|
28
|
-
const res = await fetch('/api/settings')
|
|
29
|
-
const json = await res.json()
|
|
30
|
-
return json.data
|
|
31
|
-
}
|
|
32
|
-
})
|
|
33
|
-
|
|
34
|
-
const saveKeyMutation = useMutation({
|
|
35
|
-
mutationFn: async (key: string) => {
|
|
36
|
-
const res = await fetch('/api/settings', {
|
|
37
|
-
method: 'POST',
|
|
38
|
-
headers: { 'Content-Type': 'application/json' },
|
|
39
|
-
body: JSON.stringify({ openRouterApiKey: key })
|
|
40
|
-
})
|
|
41
|
-
if (!res.ok) throw new Error('Failed to save')
|
|
42
|
-
return res.json()
|
|
43
|
-
},
|
|
44
|
-
onSuccess: () => {
|
|
45
|
-
queryClient.invalidateQueries({ queryKey: ['settings'] })
|
|
46
|
-
setApiKey('')
|
|
47
|
-
}
|
|
48
|
-
})
|
|
49
|
-
|
|
50
|
-
const deleteKeyMutation = useMutation({
|
|
51
|
-
mutationFn: async () => {
|
|
52
|
-
const res = await fetch('/api/settings', { method: 'DELETE' })
|
|
53
|
-
if (!res.ok) throw new Error('Failed to delete')
|
|
54
|
-
return res.json()
|
|
55
|
-
},
|
|
56
|
-
onSuccess: () => {
|
|
57
|
-
queryClient.invalidateQueries({ queryKey: ['settings'] })
|
|
58
|
-
}
|
|
59
|
-
})
|
|
60
|
-
|
|
61
|
-
const { data: projects } = useQuery({
|
|
62
|
-
queryKey: ['migrate-projects'],
|
|
63
|
-
queryFn: async () => {
|
|
64
|
-
const res = await fetch('/api/migrate')
|
|
65
|
-
const json = await res.json()
|
|
66
|
-
return json.data?.projects || [] as { id: string; name: string }[]
|
|
67
|
-
}
|
|
68
|
-
})
|
|
69
|
-
|
|
70
|
-
const [selectedProject, setSelectedProject] = useState<string>('')
|
|
71
|
-
const [migrationResults, setMigrationResults] = useState<{
|
|
72
|
-
success: boolean
|
|
73
|
-
results: Array<{ file: string; success: boolean; error?: string }>
|
|
74
|
-
} | null>(null)
|
|
75
|
-
|
|
76
|
-
const migrateMutation = useMutation({
|
|
77
|
-
mutationFn: async (projectId: string) => {
|
|
78
|
-
const res = await fetch('/api/migrate', {
|
|
79
|
-
method: 'POST',
|
|
80
|
-
headers: { 'Content-Type': 'application/json' },
|
|
81
|
-
body: JSON.stringify({ projectId })
|
|
82
|
-
})
|
|
83
|
-
const json = await res.json()
|
|
84
|
-
if (!res.ok) throw new Error(json.error || 'Migration failed')
|
|
85
|
-
return json.data
|
|
86
|
-
},
|
|
87
|
-
onSuccess: (data) => {
|
|
88
|
-
setMigrationResults(data)
|
|
89
|
-
},
|
|
90
|
-
onError: (error) => {
|
|
91
|
-
setMigrationResults({
|
|
92
|
-
success: false,
|
|
93
|
-
results: [{ file: 'migration', success: false, error: error.message }]
|
|
94
|
-
})
|
|
95
|
-
}
|
|
96
|
-
})
|
|
97
|
-
|
|
98
20
|
return (
|
|
99
21
|
<div className="p-6 h-full overflow-auto">
|
|
100
22
|
<header className="mb-6">
|
|
@@ -150,147 +72,6 @@ export default function Settings() {
|
|
|
150
72
|
</div>
|
|
151
73
|
</section>
|
|
152
74
|
|
|
153
|
-
{/* OpenRouter API Key */}
|
|
154
|
-
<section className="bg-card border border-border rounded-lg p-6">
|
|
155
|
-
<h2 className="text-lg font-semibold mb-4 flex items-center gap-2">
|
|
156
|
-
<Key className="w-5 h-5" />
|
|
157
|
-
OpenRouter API Key
|
|
158
|
-
</h2>
|
|
159
|
-
|
|
160
|
-
<p className="text-sm text-muted-foreground mb-4">
|
|
161
|
-
Required for AI-powered features like MD→JSON migration. Get your key at{' '}
|
|
162
|
-
<a
|
|
163
|
-
href="https://openrouter.ai/keys"
|
|
164
|
-
target="_blank"
|
|
165
|
-
rel="noopener noreferrer"
|
|
166
|
-
className="text-primary underline"
|
|
167
|
-
>
|
|
168
|
-
openrouter.ai/keys
|
|
169
|
-
</a>
|
|
170
|
-
</p>
|
|
171
|
-
|
|
172
|
-
{settings?.hasApiKey ? (
|
|
173
|
-
<div className="space-y-3">
|
|
174
|
-
<div className="flex items-center gap-3 p-3 bg-muted/50 rounded-md">
|
|
175
|
-
<CheckCircle className="w-5 h-5 text-green-500 shrink-0" />
|
|
176
|
-
<span className="text-sm font-mono">{settings.maskedKey}</span>
|
|
177
|
-
<Button
|
|
178
|
-
variant="ghost"
|
|
179
|
-
size="icon-sm"
|
|
180
|
-
onClick={() => deleteKeyMutation.mutate()}
|
|
181
|
-
disabled={deleteKeyMutation.isPending}
|
|
182
|
-
className="ml-auto text-muted-foreground hover:text-destructive"
|
|
183
|
-
>
|
|
184
|
-
{deleteKeyMutation.isPending ? (
|
|
185
|
-
<Loader2 className="w-4 h-4 animate-spin" />
|
|
186
|
-
) : (
|
|
187
|
-
<Trash2 className="w-4 h-4" />
|
|
188
|
-
)}
|
|
189
|
-
</Button>
|
|
190
|
-
</div>
|
|
191
|
-
<p className="text-xs text-muted-foreground mb-4">
|
|
192
|
-
API key is stored locally in ~/.prjct-cli/settings.json
|
|
193
|
-
</p>
|
|
194
|
-
|
|
195
|
-
{/* Data Migration Section - only shown when API key exists */}
|
|
196
|
-
<div className="pt-4 border-t border-border">
|
|
197
|
-
<h3 className="text-sm font-medium mb-3 flex items-center gap-2">
|
|
198
|
-
<Database className="w-4 h-4" />
|
|
199
|
-
Data Migration (MD → JSON)
|
|
200
|
-
</h3>
|
|
201
|
-
<p className="text-xs text-muted-foreground mb-3">
|
|
202
|
-
Convert markdown files to structured JSON using AI. This enables better parsing and querying.
|
|
203
|
-
</p>
|
|
204
|
-
|
|
205
|
-
<div className="flex gap-2 mb-3">
|
|
206
|
-
<select
|
|
207
|
-
value={selectedProject}
|
|
208
|
-
onChange={(e) => {
|
|
209
|
-
setSelectedProject(e.target.value)
|
|
210
|
-
setMigrationResults(null)
|
|
211
|
-
}}
|
|
212
|
-
className="flex-1 h-9 rounded-md border border-input bg-transparent px-3 text-sm"
|
|
213
|
-
>
|
|
214
|
-
<option value="">Select a project...</option>
|
|
215
|
-
{(projects as { id: string; name: string }[] || []).map((p) => (
|
|
216
|
-
<option key={p.id} value={p.id}>{p.name}</option>
|
|
217
|
-
))}
|
|
218
|
-
</select>
|
|
219
|
-
<Button
|
|
220
|
-
onClick={() => selectedProject && migrateMutation.mutate(selectedProject)}
|
|
221
|
-
disabled={!selectedProject || migrateMutation.isPending}
|
|
222
|
-
size="sm"
|
|
223
|
-
>
|
|
224
|
-
{migrateMutation.isPending ? (
|
|
225
|
-
<Loader2 className="w-4 h-4 animate-spin" />
|
|
226
|
-
) : (
|
|
227
|
-
<>
|
|
228
|
-
<RefreshCw className="w-4 h-4" />
|
|
229
|
-
Sync
|
|
230
|
-
</>
|
|
231
|
-
)}
|
|
232
|
-
</Button>
|
|
233
|
-
</div>
|
|
234
|
-
|
|
235
|
-
{migrationResults && (
|
|
236
|
-
<div className={`p-3 rounded-md text-sm ${migrationResults.success ? 'bg-green-500/10 border border-green-500/20' : 'bg-red-500/10 border border-red-500/20'}`}>
|
|
237
|
-
<p className={`font-medium mb-2 ${migrationResults.success ? 'text-green-500' : 'text-red-500'}`}>
|
|
238
|
-
{migrationResults.success ? 'Migration complete!' : 'Migration had errors'}
|
|
239
|
-
</p>
|
|
240
|
-
<ul className="space-y-1 text-xs">
|
|
241
|
-
{migrationResults.results.map((r, i) => (
|
|
242
|
-
<li key={i} className="flex items-center gap-2">
|
|
243
|
-
{r.success ? (
|
|
244
|
-
<CheckCircle className="w-3 h-3 text-green-500" />
|
|
245
|
-
) : (
|
|
246
|
-
<XCircle className="w-3 h-3 text-red-500" />
|
|
247
|
-
)}
|
|
248
|
-
<span>{r.file}</span>
|
|
249
|
-
{r.error && <span className="text-muted-foreground">- {r.error}</span>}
|
|
250
|
-
</li>
|
|
251
|
-
))}
|
|
252
|
-
</ul>
|
|
253
|
-
</div>
|
|
254
|
-
)}
|
|
255
|
-
</div>
|
|
256
|
-
</div>
|
|
257
|
-
) : (
|
|
258
|
-
<div className="space-y-3">
|
|
259
|
-
<div className="flex gap-2">
|
|
260
|
-
<div className="relative flex-1">
|
|
261
|
-
<Input
|
|
262
|
-
type={showKey ? 'text' : 'password'}
|
|
263
|
-
placeholder="sk-or-v1-..."
|
|
264
|
-
value={apiKey}
|
|
265
|
-
onChange={(e) => setApiKey(e.target.value)}
|
|
266
|
-
className="pr-10 font-mono"
|
|
267
|
-
/>
|
|
268
|
-
<button
|
|
269
|
-
type="button"
|
|
270
|
-
onClick={() => setShowKey(!showKey)}
|
|
271
|
-
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
|
|
272
|
-
>
|
|
273
|
-
{showKey ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
|
|
274
|
-
</button>
|
|
275
|
-
</div>
|
|
276
|
-
<Button
|
|
277
|
-
onClick={() => saveKeyMutation.mutate(apiKey)}
|
|
278
|
-
disabled={!apiKey || saveKeyMutation.isPending}
|
|
279
|
-
>
|
|
280
|
-
{saveKeyMutation.isPending ? (
|
|
281
|
-
<Loader2 className="w-4 h-4 animate-spin" />
|
|
282
|
-
) : (
|
|
283
|
-
'Save'
|
|
284
|
-
)}
|
|
285
|
-
</Button>
|
|
286
|
-
</div>
|
|
287
|
-
{saveKeyMutation.isError && (
|
|
288
|
-
<p className="text-sm text-destructive">Failed to save API key</p>
|
|
289
|
-
)}
|
|
290
|
-
</div>
|
|
291
|
-
)}
|
|
292
|
-
</section>
|
|
293
|
-
|
|
294
75
|
{/* Claude Code Status */}
|
|
295
76
|
<section className="bg-card border border-border rounded-lg p-6">
|
|
296
77
|
<h2 className="text-lg font-semibold mb-4 flex items-center gap-2">
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { BentoCard } from '@/components/BentoCard'
|
|
2
|
+
import { AlertTriangle, Clock } from 'lucide-react'
|
|
3
|
+
import { cn } from '@/lib/utils'
|
|
4
|
+
import type { BlockersCardProps } from './BlockersCard.types'
|
|
5
|
+
|
|
6
|
+
export function BlockersCard({ blockers, className }: BlockersCardProps) {
|
|
7
|
+
const hasBlockers = blockers.length > 0
|
|
8
|
+
|
|
9
|
+
return (
|
|
10
|
+
<BentoCard
|
|
11
|
+
size="1x1"
|
|
12
|
+
title="Blockers"
|
|
13
|
+
icon={AlertTriangle}
|
|
14
|
+
className={cn(
|
|
15
|
+
hasBlockers && 'border-amber-500/50 dark:border-amber-500/30',
|
|
16
|
+
className
|
|
17
|
+
)}
|
|
18
|
+
>
|
|
19
|
+
<div className="flex flex-col h-full">
|
|
20
|
+
{!hasBlockers ? (
|
|
21
|
+
<div className="flex flex-col items-center justify-center h-full text-center">
|
|
22
|
+
<div className="text-2xl mb-1">✅</div>
|
|
23
|
+
<p className="text-sm text-muted-foreground">No blockers</p>
|
|
24
|
+
<p className="text-xs text-muted-foreground mt-1">Keep the momentum!</p>
|
|
25
|
+
</div>
|
|
26
|
+
) : (
|
|
27
|
+
<>
|
|
28
|
+
<div className="flex items-center gap-2 mb-2">
|
|
29
|
+
<span className="text-2xl font-bold text-amber-600 dark:text-amber-400">
|
|
30
|
+
{blockers.length}
|
|
31
|
+
</span>
|
|
32
|
+
<span className="text-xs text-muted-foreground">
|
|
33
|
+
{blockers.length === 1 ? 'task blocked' : 'tasks blocked'}
|
|
34
|
+
</span>
|
|
35
|
+
</div>
|
|
36
|
+
|
|
37
|
+
<div className="flex-1 overflow-auto space-y-2">
|
|
38
|
+
{blockers.slice(0, 3).map((blocker, i) => (
|
|
39
|
+
<div
|
|
40
|
+
key={i}
|
|
41
|
+
className="p-2 rounded-md bg-amber-500/10 dark:bg-amber-500/5 border border-amber-500/20"
|
|
42
|
+
>
|
|
43
|
+
<p className="text-xs font-medium truncate">{blocker.task}</p>
|
|
44
|
+
<p className="text-xs text-muted-foreground truncate mt-0.5">
|
|
45
|
+
{blocker.reason}
|
|
46
|
+
</p>
|
|
47
|
+
<div className="flex items-center gap-1 mt-1">
|
|
48
|
+
<Clock className="h-3 w-3 text-muted-foreground" />
|
|
49
|
+
<span className="text-xs text-muted-foreground">
|
|
50
|
+
{blocker.daysBlocked}d
|
|
51
|
+
</span>
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
54
|
+
))}
|
|
55
|
+
</div>
|
|
56
|
+
|
|
57
|
+
{blockers.length > 3 && (
|
|
58
|
+
<p className="text-xs text-muted-foreground mt-2">
|
|
59
|
+
+{blockers.length - 3} more
|
|
60
|
+
</p>
|
|
61
|
+
)}
|
|
62
|
+
</>
|
|
63
|
+
)}
|
|
64
|
+
</div>
|
|
65
|
+
</BentoCard>
|
|
66
|
+
)
|
|
67
|
+
}
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
TooltipContent,
|
|
8
8
|
TooltipTrigger,
|
|
9
9
|
} from '@/components/ui/tooltip'
|
|
10
|
+
import { cn } from '@/lib/utils'
|
|
10
11
|
|
|
11
12
|
interface CommandButtonProps {
|
|
12
13
|
cmd: string
|
|
@@ -14,18 +15,24 @@ interface CommandButtonProps {
|
|
|
14
15
|
tip: string
|
|
15
16
|
disabled?: boolean
|
|
16
17
|
onClick: () => void
|
|
18
|
+
variant?: 'default' | 'primary'
|
|
17
19
|
}
|
|
18
20
|
|
|
19
|
-
export function CommandButton({ cmd, icon: Icon, tip, disabled, onClick }: CommandButtonProps) {
|
|
21
|
+
export function CommandButton({ cmd, icon: Icon, tip, disabled, onClick, variant = 'default' }: CommandButtonProps) {
|
|
22
|
+
const isPrimary = variant === 'primary'
|
|
23
|
+
|
|
20
24
|
return (
|
|
21
25
|
<Tooltip>
|
|
22
26
|
<TooltipTrigger asChild>
|
|
23
27
|
<Button
|
|
24
|
-
variant=
|
|
28
|
+
variant={isPrimary ? 'default' : 'ghost'}
|
|
25
29
|
size="icon"
|
|
26
30
|
onClick={onClick}
|
|
27
31
|
disabled={disabled}
|
|
28
|
-
className=
|
|
32
|
+
className={cn(
|
|
33
|
+
"h-11 w-11",
|
|
34
|
+
isPrimary && "bg-primary text-primary-foreground hover:bg-primary/90"
|
|
35
|
+
)}
|
|
29
36
|
>
|
|
30
37
|
<Icon className="w-5 h-5" />
|
|
31
38
|
</Button>
|
|
@@ -264,15 +264,25 @@ export async function getProjects() {
|
|
|
264
264
|
export async function getProject(projectId: string) {
|
|
265
265
|
const storagePath = join(GLOBAL_STORAGE, projectId)
|
|
266
266
|
|
|
267
|
-
// 1. Try to read from project.json (source of truth)
|
|
267
|
+
// 1. Try to read from project.json (source of truth for dashboard)
|
|
268
268
|
let repoPath: string | null = null
|
|
269
269
|
let name: string = projectId
|
|
270
|
+
let version: string | null = null
|
|
271
|
+
let stack: string | null = null
|
|
272
|
+
let filesCount: string | null = null
|
|
273
|
+
let commitsCount: string | null = null
|
|
274
|
+
let techStack: string[] = []
|
|
270
275
|
|
|
271
276
|
try {
|
|
272
277
|
const projectJsonPath = join(storagePath, 'project.json')
|
|
273
278
|
const projectJson = JSON.parse(await fs.readFile(projectJsonPath, 'utf-8'))
|
|
274
279
|
repoPath = projectJson.repoPath || null
|
|
275
280
|
name = projectJson.name || projectId
|
|
281
|
+
version = projectJson.version || null
|
|
282
|
+
stack = projectJson.stack || null
|
|
283
|
+
filesCount = projectJson.fileCount ? String(projectJson.fileCount) : null
|
|
284
|
+
commitsCount = projectJson.commitCount ? String(projectJson.commitCount) : null
|
|
285
|
+
techStack = projectJson.techStack || []
|
|
276
286
|
} catch {
|
|
277
287
|
// project.json doesn't exist - fallback to scan
|
|
278
288
|
if (projectPathCache.size === 0) {
|
|
@@ -308,35 +318,26 @@ export async function getProject(projectId: string) {
|
|
|
308
318
|
currentTask = await fs.readFile(nowPath, 'utf-8')
|
|
309
319
|
} catch {}
|
|
310
320
|
|
|
311
|
-
// Extract stats from claudeMd Quick Reference table
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
let techStack: string[] = []
|
|
317
|
-
|
|
318
|
-
// Parse version from | **Version** | 0.10.13 |
|
|
319
|
-
const versionMatch = claudeMd.match(/\*\*Version\*\*\s*\|\s*([^\n|]+)/)
|
|
320
|
-
if (versionMatch) version = versionMatch[1].trim()
|
|
321
|
-
|
|
322
|
-
// Parse stack from | **Stack** | Node.js CLI (CommonJS) |
|
|
323
|
-
const stackMatch = claudeMd.match(/\*\*Stack\*\*\s*\|\s*([^\n|]+)/)
|
|
324
|
-
if (stackMatch) stack = stackMatch[1].trim()
|
|
321
|
+
// Fallback: Extract stats from claudeMd Quick Reference table if not from project.json
|
|
322
|
+
if (!version) {
|
|
323
|
+
const versionMatch = claudeMd.match(/\*\*Version\*\*\s*\|\s*([^\n|]+)/)
|
|
324
|
+
if (versionMatch) version = versionMatch[1].trim()
|
|
325
|
+
}
|
|
325
326
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
327
|
+
if (!stack) {
|
|
328
|
+
const stackMatch = claudeMd.match(/\*\*Stack\*\*\s*\|\s*([^\n|]+)/)
|
|
329
|
+
if (stackMatch) stack = stackMatch[1].trim()
|
|
330
|
+
}
|
|
329
331
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
332
|
+
if (!filesCount) {
|
|
333
|
+
const filesMatch = claudeMd.match(/\*\*Files\*\*\s*\|\s*([^\n|]+)/)
|
|
334
|
+
if (filesMatch) filesCount = filesMatch[1].trim()
|
|
335
|
+
}
|
|
333
336
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
techStack = projectJson.techStack || []
|
|
339
|
-
} catch {}
|
|
337
|
+
if (!commitsCount) {
|
|
338
|
+
const commitsMatch = claudeMd.match(/\*\*Commits\*\*\s*\|\s*([^\n|]+)/)
|
|
339
|
+
if (commitsMatch) commitsCount = commitsMatch[1].trim()
|
|
340
|
+
}
|
|
340
341
|
|
|
341
342
|
// Find favicon/icon in project repo
|
|
342
343
|
let iconPath: string | null = null
|
|
@@ -1,44 +1,48 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Projects Service (Server-only)
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* No
|
|
4
|
+
* MD-First Architecture: Reads directly from MD files.
|
|
5
|
+
* No JSON fallback - MD is the source of truth.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import 'server-only'
|
|
9
9
|
import { cache } from 'react'
|
|
10
|
-
import {
|
|
11
|
-
import { getProjects as getProjectsList, getProject as getProjectLegacy } from '@/lib/projects'
|
|
10
|
+
import { getProjects as getProjectsList, getProject as getMdProject } from '@/lib/projects'
|
|
12
11
|
|
|
13
|
-
|
|
12
|
+
// Types for project data
|
|
13
|
+
export interface ProjectJson {
|
|
14
|
+
projectId: string
|
|
15
|
+
name: string
|
|
16
|
+
repoPath?: string | null
|
|
17
|
+
techStack: string[]
|
|
18
|
+
fileCount: number
|
|
19
|
+
commitCount: number
|
|
20
|
+
createdAt: string
|
|
21
|
+
lastSync: string
|
|
22
|
+
}
|
|
14
23
|
|
|
15
24
|
/**
|
|
16
25
|
* Get single project by ID - cached per request
|
|
26
|
+
*
|
|
27
|
+
* MD-First: Uses MD files as source of truth
|
|
17
28
|
*/
|
|
18
29
|
export const getProject = cache(async (projectId: string): Promise<ProjectJson | null> => {
|
|
19
|
-
// Try JSON first
|
|
20
|
-
const jsonProject = await loadProject(projectId)
|
|
21
|
-
if (jsonProject) {
|
|
22
|
-
return jsonProject
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
// Fallback to legacy
|
|
26
30
|
try {
|
|
27
|
-
const
|
|
28
|
-
if (
|
|
31
|
+
const project = await getMdProject(projectId)
|
|
32
|
+
if (project) {
|
|
29
33
|
return {
|
|
30
|
-
projectId:
|
|
31
|
-
name:
|
|
32
|
-
repoPath:
|
|
33
|
-
techStack:
|
|
34
|
-
fileCount:
|
|
35
|
-
commitCount:
|
|
34
|
+
projectId: project.id,
|
|
35
|
+
name: project.name,
|
|
36
|
+
repoPath: project.repoPath,
|
|
37
|
+
techStack: project.techStack || [],
|
|
38
|
+
fileCount: project.filesCount ? parseInt(project.filesCount) : 0,
|
|
39
|
+
commitCount: project.commitsCount ? parseInt(project.commitsCount) : 0,
|
|
36
40
|
createdAt: new Date().toISOString(),
|
|
37
41
|
lastSync: new Date().toISOString()
|
|
38
42
|
}
|
|
39
43
|
}
|
|
40
44
|
} catch {
|
|
41
|
-
//
|
|
45
|
+
// Project not found
|
|
42
46
|
}
|
|
43
47
|
|
|
44
48
|
return null
|