prjct-cli 0.12.1 → 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.
Files changed (38) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/CLAUDE.md +18 -6
  3. package/bin/serve.js +12 -30
  4. package/core/data/index.ts +19 -5
  5. package/core/data/md-base-manager.ts +203 -0
  6. package/core/data/md-queue-manager.ts +179 -0
  7. package/core/data/md-state-manager.ts +133 -0
  8. package/core/serializers/index.ts +20 -0
  9. package/core/serializers/queue-serializer.ts +210 -0
  10. package/core/serializers/state-serializer.ts +136 -0
  11. package/core/utils/file-helper.ts +12 -0
  12. package/package.json +1 -1
  13. package/packages/web/app/api/projects/[id]/stats/route.ts +6 -29
  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/AppSidebar/AppSidebar.tsx +5 -3
  18. package/packages/web/components/BlockersCard/BlockersCard.tsx +67 -0
  19. package/packages/web/components/BlockersCard/BlockersCard.types.ts +11 -0
  20. package/packages/web/components/BlockersCard/index.ts +2 -0
  21. package/packages/web/components/CommandButton/CommandButton.tsx +10 -3
  22. package/packages/web/lib/projects.ts +28 -27
  23. package/packages/web/lib/services/projects.server.ts +25 -21
  24. package/packages/web/lib/services/stats.server.ts +355 -57
  25. package/packages/web/package.json +0 -2
  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/lib/json-loader.ts +0 -630
  38. package/packages/web/lib/services/migration.server.ts +0 -600
@@ -1,630 +0,0 @@
1
- /**
2
- * JSON Loader
3
- *
4
- * Loads project data directly from JSON files in data/ directory.
5
- * JSON is source of truth, MD is generated for Claude.
6
- *
7
- * New Architecture:
8
- * ~/.prjct-cli/projects/{projectId}/
9
- * └── data/ # JSON source of truth
10
- * ├── state.json # Current task
11
- * ├── queue.json # Task queue
12
- * ├── ideas.json # Ideas
13
- * ├── roadmap.json # Features
14
- * ├── shipped.json # Shipped items
15
- * ├── metrics.json # Stats
16
- * └── project.json # Metadata
17
- */
18
-
19
- import { promises as fs } from 'fs'
20
- import { join } from 'path'
21
- import { homedir } from 'os'
22
-
23
- const GLOBAL_STORAGE = join(homedir(), '.prjct-cli', 'projects')
24
-
25
- // ============================================
26
- // TYPES - Match enriched schemas from migration.server.ts
27
- // ============================================
28
-
29
- export type Priority = 'low' | 'medium' | 'high' | 'critical'
30
- export type TaskType = 'feature' | 'bug' | 'improvement' | 'chore'
31
- export type TaskSection = 'active' | 'backlog' | 'previously_active'
32
-
33
- // Duration object for parsed time strings like "13h 38m"
34
- export interface Duration {
35
- hours: number
36
- minutes: number
37
- totalMinutes: number
38
- }
39
-
40
- // Code metrics from "Files: 4 | +160/-31 | Commits: 0"
41
- export interface CodeMetrics {
42
- filesChanged?: number | null
43
- linesAdded?: number | null
44
- linesRemoved?: number | null
45
- commits?: number | null
46
- }
47
-
48
- // Git commit information
49
- export interface CommitInfo {
50
- hash?: string
51
- message?: string
52
- branch?: string
53
- }
54
-
55
- export interface CurrentTask {
56
- id: string
57
- description: string
58
- startedAt: string
59
- sessionId: string
60
- featureId?: string
61
- }
62
-
63
- export interface PreviousTask {
64
- id: string
65
- description: string
66
- status: 'paused'
67
- startedAt: string
68
- pausedAt: string
69
- }
70
-
71
- export interface StateJson {
72
- currentTask: CurrentTask | null
73
- previousTask?: PreviousTask | null
74
- lastUpdated: string
75
- }
76
-
77
- export interface QueueTask {
78
- id: string
79
- description: string
80
- priority: Priority
81
- type: TaskType
82
- featureId?: string
83
- originFeature?: string
84
- completed: boolean
85
- completedAt?: string
86
- createdAt: string
87
- section: TaskSection
88
- // ZERO DATA LOSS fields
89
- agent?: string // "fe", "be", "fe + be"
90
- groupName?: string // "Sales Reports", "Stock Audits"
91
- groupId?: string // For grouping related tasks
92
- }
93
-
94
- export interface QueueJson {
95
- tasks: QueueTask[]
96
- lastUpdated: string
97
- }
98
-
99
- export type IdeaPriority = 'low' | 'medium' | 'high'
100
- export type IdeaStatus = 'pending' | 'converted' | 'completed' | 'archived'
101
-
102
- export interface ImpactEffort {
103
- impact: 'high' | 'medium' | 'low'
104
- effort: 'high' | 'medium' | 'low'
105
- }
106
-
107
- // Tech stack definition for idea specs
108
- export interface TechStack {
109
- frontend?: string
110
- backend?: string
111
- payments?: string
112
- ai?: string
113
- deploy?: string
114
- other?: string[]
115
- }
116
-
117
- // Module definition for complex ideas
118
- export interface IdeaModule {
119
- name: string
120
- description: string
121
- }
122
-
123
- // Role definition
124
- export interface IdeaRole {
125
- name: string
126
- description?: string
127
- }
128
-
129
- export interface IdeaSchema {
130
- id: string
131
- text: string
132
- details?: string
133
- priority: IdeaPriority
134
- status: IdeaStatus
135
- tags: string[]
136
- addedAt: string
137
- completedAt?: string
138
- convertedTo?: string
139
- // Source documentation
140
- source?: string
141
- sourceFiles?: string[]
142
- // Enriched fields from MD
143
- painPoints?: string[]
144
- solutions?: string[]
145
- filesAffected?: string[]
146
- impactEffort?: ImpactEffort
147
- implementationNotes?: string
148
- // Technical spec fields for ZERO DATA LOSS
149
- stack?: TechStack
150
- modules?: IdeaModule[]
151
- roles?: IdeaRole[]
152
- risks?: string[]
153
- risksCount?: number
154
- }
155
-
156
- export interface IdeasJson {
157
- ideas: IdeaSchema[]
158
- lastUpdated: string
159
- }
160
-
161
- export type FeatureStatus = 'planned' | 'active' | 'completed' | 'shipped'
162
- export type FeatureImpact = 'low' | 'medium' | 'high'
163
- export type FeatureType = 'feature' | 'breaking_change' | 'refactor' | 'infrastructure'
164
-
165
- export interface FeatureTask {
166
- id: string
167
- description: string
168
- completed: boolean
169
- completedAt?: string
170
- }
171
-
172
- export interface RoadmapPhase {
173
- id: string
174
- name: string
175
- status: 'completed' | 'active' | 'planned'
176
- completedAt?: string
177
- }
178
-
179
- export interface RoadmapStrategy {
180
- goal: string
181
- phases: RoadmapPhase[]
182
- successMetrics?: string[]
183
- }
184
-
185
- // Duration for completed sprints/features
186
- export interface FeatureDuration {
187
- hours: number
188
- minutes: number
189
- totalMinutes: number
190
- display?: string
191
- }
192
-
193
- export interface FeatureSchema {
194
- id: string
195
- name: string
196
- description?: string
197
- date: string
198
- status: FeatureStatus
199
- impact: FeatureImpact
200
- effort?: string
201
- progress: number
202
- // Enriched fields from MD
203
- type?: FeatureType
204
- roi?: number // 1-5 from star count
205
- why?: string[]
206
- technicalNotes?: string[]
207
- compatibility?: string
208
- phase?: string
209
- tasks: FeatureTask[]
210
- createdAt: string
211
- shippedAt?: string
212
- version?: string
213
- // ZERO DATA LOSS - additional fields
214
- duration?: FeatureDuration
215
- taskCount?: number
216
- agent?: string
217
- sprintName?: string
218
- completedDate?: string
219
- }
220
-
221
- export interface RoadmapJson {
222
- strategy?: RoadmapStrategy | null
223
- features: FeatureSchema[]
224
- backlog: string[]
225
- lastUpdated: string
226
- }
227
-
228
- export type ShipType = 'feature' | 'fix' | 'improvement' | 'refactor'
229
- export type CheckStatus = 'pass' | 'warning' | 'fail' | 'skipped'
230
-
231
- export interface ShipChange {
232
- description: string
233
- type?: 'added' | 'changed' | 'fixed' | 'removed'
234
- }
235
-
236
- export interface QualityMetrics {
237
- lintStatus?: CheckStatus | null
238
- lintDetails?: string
239
- testStatus?: CheckStatus | null
240
- testDetails?: string
241
- }
242
-
243
- export type AgentType = 'fe' | 'be' | 'fe+be' | 'devops' | 'ai' | string
244
-
245
- export interface ShippedItemSchema {
246
- id: string
247
- name: string
248
- version?: string | null
249
- type: ShipType
250
- // Agent who worked on this
251
- agent?: AgentType
252
- // Full description (narrative text, not just bullet points)
253
- description?: string
254
- // Changelog from bullet points
255
- changes: ShipChange[]
256
- // Code snippets if any
257
- codeSnippets?: string[]
258
- // Git commit info
259
- commit?: CommitInfo
260
- // Enriched fields from MD
261
- codeMetrics?: CodeMetrics
262
- qualityMetrics?: QualityMetrics
263
- quantitativeImpact?: string
264
- duration?: Duration
265
- tasksCompleted?: number | null
266
- shippedAt: string
267
- featureId?: string
268
- }
269
-
270
- export interface ShippedJson {
271
- items: ShippedItemSchema[]
272
- lastUpdated: string
273
- }
274
-
275
- export interface RecentActivity {
276
- timestamp: string
277
- action: 'started' | 'completed' | 'shipped' | 'paused'
278
- description: string
279
- duration?: { hours: number; minutes: number }
280
- codeChanges?: CodeMetrics
281
- }
282
-
283
- export interface MetricsJson {
284
- currentSprint: {
285
- tasksStarted: number
286
- tasksCompleted: number
287
- inProgress: number
288
- }
289
- allTime: {
290
- featuresShipped: number
291
- tasksCompleted: number
292
- totalTimeTracked: Duration
293
- daysActive: number
294
- }
295
- velocity: {
296
- featuresPerWeek: number
297
- tasksPerDay: number
298
- }
299
- recentActivity: RecentActivity[]
300
- lastUpdated: string
301
- }
302
-
303
- export interface ProjectJson {
304
- projectId: string
305
- name: string
306
- repoPath: string
307
- description?: string
308
- version?: string
309
- techStack: string[]
310
- fileCount: number
311
- commitCount: number
312
- createdAt: string
313
- lastSync: string
314
- }
315
-
316
- export interface AgentJson {
317
- name: string
318
- description: string
319
- skills: string[]
320
- patterns: string[]
321
- filesOwned: string[]
322
- successRate?: number
323
- tasksCompleted?: number
324
- bestFor: string[]
325
- avoidFor: string[]
326
- }
327
-
328
- export interface AnalysisJson {
329
- projectId: string
330
- languages: string[]
331
- frameworks: string[]
332
- packageManager?: string
333
- sourceDir?: string
334
- testDir?: string
335
- configFiles: string[]
336
- fileCount: number
337
- patterns: Array<{ name: string; description: string; location?: string }>
338
- antiPatterns: Array<{ issue: string; file: string; suggestion: string }>
339
- analyzedAt: string
340
- }
341
-
342
- export interface OutcomeJson {
343
- id: string
344
- taskId: string
345
- description: string
346
- estimatedDuration?: string
347
- actualDuration: string
348
- completedAsPlanned: boolean
349
- qualityScore: 1 | 2 | 3 | 4 | 5
350
- blockers: string[]
351
- agentUsed?: string
352
- completedAt: string
353
- }
354
-
355
- // ============================================
356
- // HELPERS
357
- // ============================================
358
-
359
- async function readJsonFile<T>(filePath: string, defaultValue: T): Promise<T> {
360
- try {
361
- const content = await fs.readFile(filePath, 'utf-8')
362
- return JSON.parse(content) as T
363
- } catch {
364
- return defaultValue
365
- }
366
- }
367
-
368
- async function fileExists(filePath: string): Promise<boolean> {
369
- try {
370
- await fs.access(filePath)
371
- return true
372
- } catch {
373
- return false
374
- }
375
- }
376
-
377
- function getDataPath(projectId: string): string {
378
- return join(GLOBAL_STORAGE, projectId, 'data')
379
- }
380
-
381
- function getProjectPath(projectId: string): string {
382
- return join(GLOBAL_STORAGE, projectId)
383
- }
384
-
385
- // ============================================
386
- // LOADERS - Read from data/ directory
387
- // ============================================
388
-
389
- export async function loadState(projectId: string): Promise<StateJson | null> {
390
- const filePath = join(getDataPath(projectId), 'state.json')
391
- if (!await fileExists(filePath)) return null
392
- return readJsonFile<StateJson>(filePath, null as unknown as StateJson)
393
- }
394
-
395
- export async function loadQueue(projectId: string): Promise<QueueJson | null> {
396
- const filePath = join(getDataPath(projectId), 'queue.json')
397
- if (!await fileExists(filePath)) return null
398
- return readJsonFile<QueueJson>(filePath, null as unknown as QueueJson)
399
- }
400
-
401
- export async function loadIdeas(projectId: string): Promise<IdeasJson | null> {
402
- const filePath = join(getDataPath(projectId), 'ideas.json')
403
- if (!await fileExists(filePath)) return null
404
- return readJsonFile<IdeasJson>(filePath, null as unknown as IdeasJson)
405
- }
406
-
407
- export async function loadRoadmap(projectId: string): Promise<RoadmapJson | null> {
408
- const filePath = join(getDataPath(projectId), 'roadmap.json')
409
- if (!await fileExists(filePath)) return null
410
- return readJsonFile<RoadmapJson>(filePath, null as unknown as RoadmapJson)
411
- }
412
-
413
- export async function loadShipped(projectId: string): Promise<ShippedJson | null> {
414
- const filePath = join(getDataPath(projectId), 'shipped.json')
415
- if (!await fileExists(filePath)) return null
416
- return readJsonFile<ShippedJson>(filePath, null as unknown as ShippedJson)
417
- }
418
-
419
- export async function loadMetrics(projectId: string): Promise<MetricsJson | null> {
420
- const filePath = join(getDataPath(projectId), 'metrics.json')
421
- if (!await fileExists(filePath)) return null
422
- return readJsonFile<MetricsJson>(filePath, null as unknown as MetricsJson)
423
- }
424
-
425
- export async function loadProject(projectId: string): Promise<ProjectJson | null> {
426
- // Try data/ first, then root for backwards compatibility
427
- let filePath = join(getDataPath(projectId), 'project.json')
428
- if (!await fileExists(filePath)) {
429
- filePath = join(getProjectPath(projectId), 'project.json')
430
- }
431
- if (!await fileExists(filePath)) return null
432
- return readJsonFile<ProjectJson>(filePath, null as unknown as ProjectJson)
433
- }
434
-
435
- export async function loadAgents(projectId: string): Promise<AgentJson[]> {
436
- const filePath = join(getDataPath(projectId), 'agents.json')
437
- return readJsonFile<AgentJson[]>(filePath, [])
438
- }
439
-
440
- export async function loadAnalysis(projectId: string): Promise<AnalysisJson | null> {
441
- const filePath = join(getDataPath(projectId), 'analysis.json')
442
- if (!await fileExists(filePath)) return null
443
- return readJsonFile<AnalysisJson>(filePath, null as unknown as AnalysisJson)
444
- }
445
-
446
- export async function loadOutcomes(projectId: string): Promise<OutcomeJson[]> {
447
- const filePath = join(getDataPath(projectId), 'outcomes.json')
448
- return readJsonFile<OutcomeJson[]>(filePath, [])
449
- }
450
-
451
- // ============================================
452
- // UNIFIED LOADER
453
- // ============================================
454
-
455
- export interface UnifiedJsonData {
456
- state: StateJson | null
457
- queue: QueueJson | null
458
- project: ProjectJson | null
459
- agents: AgentJson[]
460
- ideas: IdeasJson | null
461
- roadmap: RoadmapJson | null
462
- shipped: ShippedJson | null
463
- metrics: MetricsJson | null
464
- analysis: AnalysisJson | null
465
- outcomes: OutcomeJson[]
466
- // Computed
467
- insights: ProjectInsights
468
- hasJsonData: boolean
469
- }
470
-
471
- export interface ProjectInsights {
472
- healthScore: number
473
- estimateAccuracy: number
474
- topBlockers: string[]
475
- patternsDetected: string[]
476
- recommendations: string[]
477
- }
478
-
479
- function computeInsights(data: {
480
- state: StateJson | null
481
- queue: QueueJson | null
482
- metrics: MetricsJson | null
483
- outcomes: OutcomeJson[]
484
- agents: AgentJson[]
485
- roadmap: RoadmapJson | null
486
- }): ProjectInsights {
487
- let healthScore = 50
488
-
489
- // State-based scoring
490
- if (data.state?.currentTask) {
491
- healthScore += 10
492
- }
493
-
494
- // Metrics-based scoring
495
- if (data.metrics) {
496
- healthScore += Math.min(10, (data.metrics.velocity?.tasksPerDay || 0) * 2)
497
- healthScore += Math.min(5, data.metrics.allTime?.daysActive || 0)
498
- }
499
-
500
- // Queue scoring
501
- if (data.queue) {
502
- const pendingTasks = data.queue.tasks.filter(t => !t.completed).length
503
- if (pendingTasks > 15) healthScore -= 5
504
- if (pendingTasks < 5 && pendingTasks > 0) healthScore += 5
505
- }
506
-
507
- // Outcomes-based scoring
508
- const outcomes = data.outcomes
509
- let estimateAccuracy = 0
510
- const topBlockers: string[] = []
511
- const patternsDetected: string[] = []
512
-
513
- if (outcomes.length > 0) {
514
- const avgQuality = outcomes.reduce((sum, o) => sum + o.qualityScore, 0) / outcomes.length
515
- healthScore += Math.round(avgQuality * 2)
516
-
517
- const completedAsPlanned = outcomes.filter(o => o.completedAsPlanned).length
518
- estimateAccuracy = Math.round((completedAsPlanned / outcomes.length) * 100)
519
- healthScore += Math.round(estimateAccuracy * 0.1)
520
-
521
- // Count blockers
522
- const blockerCounts = new Map<string, number>()
523
- for (const o of outcomes) {
524
- for (const b of o.blockers) {
525
- blockerCounts.set(b, (blockerCounts.get(b) || 0) + 1)
526
- }
527
- }
528
- topBlockers.push(
529
- ...Array.from(blockerCounts.entries())
530
- .sort((a, b) => b[1] - a[1])
531
- .slice(0, 5)
532
- .map(([b]) => b)
533
- )
534
-
535
- // Detect patterns
536
- const underestimated = outcomes.filter(o => !o.completedAsPlanned).length
537
- if (underestimated / outcomes.length > 0.6) {
538
- patternsDetected.push('Tasks often take longer than estimated')
539
- }
540
- }
541
-
542
- // Agents-based scoring
543
- if (data.agents.length > 0) {
544
- const avgSuccess = data.agents
545
- .filter(a => a.successRate !== undefined)
546
- .reduce((sum, a) => sum + (a.successRate || 0), 0)
547
- if (avgSuccess > 0) {
548
- healthScore += Math.round(avgSuccess * 0.1 / data.agents.length)
549
- }
550
- }
551
-
552
- // Roadmap progress
553
- if (data.roadmap && data.roadmap.features.length > 0) {
554
- const completed = data.roadmap.features.filter(f => f.status === 'completed' || f.status === 'shipped').length
555
- const progress = Math.round((completed / data.roadmap.features.length) * 100)
556
- if (progress > 50) healthScore += 5
557
- }
558
-
559
- healthScore = Math.max(0, Math.min(100, healthScore))
560
-
561
- // Recommendations
562
- const recommendations: string[] = []
563
- if (!data.state?.currentTask) {
564
- recommendations.push('Start a task with /p:now')
565
- }
566
- if (data.queue && data.queue.tasks.filter(t => !t.completed).length > 10) {
567
- recommendations.push('Queue is large - prioritize tasks')
568
- }
569
- if (estimateAccuracy < 50 && outcomes.length > 5) {
570
- recommendations.push('Add buffer to estimates')
571
- }
572
- if (data.agents.length === 0) {
573
- recommendations.push('Run /p:sync to generate agents')
574
- }
575
-
576
- return {
577
- healthScore,
578
- estimateAccuracy,
579
- topBlockers,
580
- patternsDetected,
581
- recommendations: recommendations.slice(0, 4)
582
- }
583
- }
584
-
585
- export async function loadUnifiedJsonData(projectId: string): Promise<UnifiedJsonData> {
586
- const [state, queue, project, agents, ideas, roadmap, shipped, metrics, analysis, outcomes] = await Promise.all([
587
- loadState(projectId),
588
- loadQueue(projectId),
589
- loadProject(projectId),
590
- loadAgents(projectId),
591
- loadIdeas(projectId),
592
- loadRoadmap(projectId),
593
- loadShipped(projectId),
594
- loadMetrics(projectId),
595
- loadAnalysis(projectId),
596
- loadOutcomes(projectId)
597
- ])
598
-
599
- const insights = computeInsights({ state, queue, metrics, outcomes, agents, roadmap })
600
-
601
- // Check if we have any JSON data
602
- const hasJsonData = state !== null || project !== null || queue !== null
603
-
604
- return {
605
- state,
606
- queue,
607
- project,
608
- agents,
609
- ideas,
610
- roadmap,
611
- shipped,
612
- metrics,
613
- analysis,
614
- outcomes,
615
- insights,
616
- hasJsonData
617
- }
618
- }
619
-
620
- export async function hasJsonState(projectId: string): Promise<boolean> {
621
- const statePath = join(getDataPath(projectId), 'state.json')
622
- const projectPath = join(getDataPath(projectId), 'project.json')
623
-
624
- const [hasState, hasProject] = await Promise.all([
625
- fileExists(statePath),
626
- fileExists(projectPath)
627
- ])
628
-
629
- return hasState || hasProject
630
- }