prjct-cli 0.12.2 → 0.13.1

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.
Files changed (40) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/CLAUDE.md +18 -6
  3. package/core/data/index.ts +19 -5
  4. package/core/data/md-base-manager.ts +203 -0
  5. package/core/data/md-queue-manager.ts +179 -0
  6. package/core/data/md-state-manager.ts +133 -0
  7. package/core/serializers/index.ts +20 -0
  8. package/core/serializers/queue-serializer.ts +210 -0
  9. package/core/serializers/state-serializer.ts +136 -0
  10. package/core/utils/file-helper.ts +12 -0
  11. package/package.json +1 -1
  12. package/packages/web/app/api/projects/[id]/stats/route.ts +6 -29
  13. package/packages/web/app/page.tsx +1 -6
  14. package/packages/web/app/project/[id]/page.tsx +34 -1
  15. package/packages/web/app/project/[id]/stats/page.tsx +11 -5
  16. package/packages/web/app/settings/page.tsx +2 -221
  17. package/packages/web/components/BlockersCard/BlockersCard.tsx +67 -0
  18. package/packages/web/components/BlockersCard/BlockersCard.types.ts +11 -0
  19. package/packages/web/components/BlockersCard/index.ts +2 -0
  20. package/packages/web/components/CommandButton/CommandButton.tsx +10 -3
  21. package/packages/web/lib/projects.ts +28 -27
  22. package/packages/web/lib/services/projects.server.ts +25 -21
  23. package/packages/web/lib/services/stats.server.ts +355 -57
  24. package/packages/web/next-env.d.ts +1 -1
  25. package/packages/web/package.json +0 -4
  26. package/templates/commands/decision.md +226 -0
  27. package/templates/commands/done.md +100 -68
  28. package/templates/commands/feature.md +102 -103
  29. package/templates/commands/idea.md +41 -38
  30. package/templates/commands/now.md +94 -33
  31. package/templates/commands/pause.md +90 -30
  32. package/templates/commands/ship.md +179 -74
  33. package/templates/commands/sync.md +324 -200
  34. package/packages/web/app/api/migrate/route.ts +0 -46
  35. package/packages/web/app/api/settings/route.ts +0 -97
  36. package/packages/web/app/api/v2/projects/[id]/unified/route.ts +0 -57
  37. package/packages/web/components/MigrationGate/MigrationGate.tsx +0 -304
  38. package/packages/web/components/MigrationGate/index.ts +0 -1
  39. package/packages/web/lib/json-loader.ts +0 -630
  40. 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'