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.
- 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/next-env.d.ts +1 -1
- package/packages/web/package.json +0 -4
- 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,27 +1,110 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Stats 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
10
|
import { exec } from 'child_process'
|
|
11
11
|
import { promisify } from 'util'
|
|
12
|
-
import {
|
|
13
|
-
loadUnifiedJsonData,
|
|
14
|
-
hasJsonState,
|
|
15
|
-
type UnifiedJsonData,
|
|
16
|
-
type StateJson,
|
|
17
|
-
type QueueJson,
|
|
18
|
-
type MetricsJson,
|
|
19
|
-
type ProjectInsights,
|
|
20
|
-
} from '@/lib/json-loader'
|
|
21
|
-
import { getProjectStats as getLegacyStats, type ProjectStats } from '@/lib/parse-prjct-files'
|
|
12
|
+
import { getProjectStats as getMdStats, type ProjectStats, type SessionDay } from '@/lib/parse-prjct-files'
|
|
22
13
|
import { getProjects } from './projects.server'
|
|
23
14
|
|
|
24
|
-
|
|
15
|
+
// Types for MD-based stats
|
|
16
|
+
export interface StateJson {
|
|
17
|
+
currentTask: {
|
|
18
|
+
id?: string
|
|
19
|
+
description: string
|
|
20
|
+
startedAt?: string
|
|
21
|
+
sessionId?: string
|
|
22
|
+
feature?: string
|
|
23
|
+
agent?: string
|
|
24
|
+
} | null
|
|
25
|
+
previousTask?: {
|
|
26
|
+
id?: string
|
|
27
|
+
description: string
|
|
28
|
+
status: string
|
|
29
|
+
startedAt?: string
|
|
30
|
+
pausedAt?: string
|
|
31
|
+
} | null
|
|
32
|
+
lastUpdated?: string
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface QueueTask {
|
|
36
|
+
id: string
|
|
37
|
+
description: string
|
|
38
|
+
priority: 'low' | 'medium' | 'high' | 'critical'
|
|
39
|
+
type: 'feature' | 'bug' | 'improvement' | 'chore'
|
|
40
|
+
completed: boolean
|
|
41
|
+
createdAt: string
|
|
42
|
+
completedAt?: string
|
|
43
|
+
section: 'active' | 'backlog' | 'previously_active'
|
|
44
|
+
agent?: string
|
|
45
|
+
originFeature?: string
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface QueueJson {
|
|
49
|
+
tasks: QueueTask[]
|
|
50
|
+
lastUpdated: string
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface MetricsJson {
|
|
54
|
+
recentActivity?: Array<{ timestamp: string; type?: string; description?: string; action?: string }>
|
|
55
|
+
velocity?: { tasksPerDay?: number }
|
|
56
|
+
currentSprint?: { tasksCompleted?: number }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface Blocker {
|
|
60
|
+
task: string
|
|
61
|
+
reason: string
|
|
62
|
+
since: string
|
|
63
|
+
daysBlocked: number
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface ProjectInsights {
|
|
67
|
+
healthScore: number
|
|
68
|
+
estimateAccuracy: number
|
|
69
|
+
blockers: Blocker[]
|
|
70
|
+
recommendations: string[]
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface RoadmapFeature {
|
|
74
|
+
name: string
|
|
75
|
+
status?: 'pending' | 'active' | 'shipped' | 'completed'
|
|
76
|
+
tasks: Array<{
|
|
77
|
+
description: string
|
|
78
|
+
completed: boolean
|
|
79
|
+
}>
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface ShippedItem {
|
|
83
|
+
name: string
|
|
84
|
+
date?: string
|
|
85
|
+
shippedAt?: string
|
|
86
|
+
duration?: string
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export interface UnifiedJsonData {
|
|
90
|
+
state: StateJson | null
|
|
91
|
+
queue: QueueJson | null
|
|
92
|
+
metrics: MetricsJson | null
|
|
93
|
+
insights: ProjectInsights
|
|
94
|
+
agents: Array<{
|
|
95
|
+
name: string
|
|
96
|
+
role?: string
|
|
97
|
+
description?: string
|
|
98
|
+
successRate?: number
|
|
99
|
+
tasksCompleted?: number
|
|
100
|
+
bestFor?: string[]
|
|
101
|
+
}>
|
|
102
|
+
ideas: { ideas: Array<{ text: string; status?: string; priority?: string }> } | null
|
|
103
|
+
roadmap: { features: RoadmapFeature[] } | null
|
|
104
|
+
shipped: { items: ShippedItem[] } | null
|
|
105
|
+
outcomes: Array<{ type: string }>
|
|
106
|
+
hasJsonData: boolean
|
|
107
|
+
}
|
|
25
108
|
|
|
26
109
|
// Activity type for recent activity tracking
|
|
27
110
|
export interface RecentActivity {
|
|
@@ -86,8 +169,7 @@ export interface StatsResult {
|
|
|
86
169
|
const DEFAULT_INSIGHTS: ProjectInsights = {
|
|
87
170
|
healthScore: 0,
|
|
88
171
|
estimateAccuracy: 0,
|
|
89
|
-
|
|
90
|
-
patternsDetected: [],
|
|
172
|
+
blockers: [],
|
|
91
173
|
recommendations: ['Run /p:sync to initialize project']
|
|
92
174
|
}
|
|
93
175
|
|
|
@@ -106,47 +188,273 @@ const EMPTY_STATS_RESULT: StatsResult = {
|
|
|
106
188
|
}
|
|
107
189
|
|
|
108
190
|
/**
|
|
109
|
-
*
|
|
191
|
+
* Calculate estimate accuracy from sessions
|
|
192
|
+
* Returns percentage of tasks completed within ±20% of estimate
|
|
110
193
|
*/
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
194
|
+
function calculateEstimateAccuracy(sessions: SessionDay[]): number {
|
|
195
|
+
let tasksWithEstimate = 0
|
|
196
|
+
let accurateTasks = 0
|
|
197
|
+
|
|
198
|
+
for (const session of sessions) {
|
|
199
|
+
for (const event of session.events) {
|
|
200
|
+
// Look for task_complete events with estimate and actual duration
|
|
201
|
+
if (event.type === 'task_complete' || event.type === 'session_completed') {
|
|
202
|
+
const e = event as { estimate?: string | number; duration?: string | number; actual?: number }
|
|
203
|
+
if (e.estimate && (e.duration || e.actual)) {
|
|
204
|
+
tasksWithEstimate++
|
|
205
|
+
|
|
206
|
+
// Parse estimate (e.g., "2h" -> 7200 seconds, or raw number)
|
|
207
|
+
const estimateSec = typeof e.estimate === 'number'
|
|
208
|
+
? e.estimate
|
|
209
|
+
: parseTimeToSeconds(String(e.estimate))
|
|
210
|
+
|
|
211
|
+
// Parse actual
|
|
212
|
+
const actualSec = typeof e.actual === 'number'
|
|
213
|
+
? e.actual
|
|
214
|
+
: typeof e.duration === 'number'
|
|
215
|
+
? e.duration
|
|
216
|
+
: parseTimeToSeconds(String(e.duration || '0'))
|
|
217
|
+
|
|
218
|
+
if (estimateSec > 0 && actualSec > 0) {
|
|
219
|
+
const ratio = actualSec / estimateSec
|
|
220
|
+
// Within ±20% is "accurate"
|
|
221
|
+
if (ratio >= 0.8 && ratio <= 1.2) {
|
|
222
|
+
accurateTasks++
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return tasksWithEstimate > 0
|
|
231
|
+
? Math.round((accurateTasks / tasksWithEstimate) * 100)
|
|
232
|
+
: 0
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Parse time string to seconds (e.g., "2h" -> 7200, "30m" -> 1800)
|
|
237
|
+
*/
|
|
238
|
+
function parseTimeToSeconds(time: string): number {
|
|
239
|
+
const hours = time.match(/(\d+(?:\.\d+)?)\s*h/i)
|
|
240
|
+
const minutes = time.match(/(\d+(?:\.\d+)?)\s*m/i)
|
|
241
|
+
const seconds = time.match(/(\d+(?:\.\d+)?)\s*s/i)
|
|
242
|
+
|
|
243
|
+
let total = 0
|
|
244
|
+
if (hours) total += parseFloat(hours[1]) * 3600
|
|
245
|
+
if (minutes) total += parseFloat(minutes[1]) * 60
|
|
246
|
+
if (seconds) total += parseFloat(seconds[1])
|
|
247
|
+
|
|
248
|
+
// If just a number, assume seconds
|
|
249
|
+
if (total === 0 && /^\d+$/.test(time.trim())) {
|
|
250
|
+
total = parseInt(time.trim())
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return total
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Extract blockers from timeline events
|
|
258
|
+
*/
|
|
259
|
+
function extractBlockers(timeline: Array<{ ts: string; type: string }>): Blocker[] {
|
|
260
|
+
const blockers: Blocker[] = []
|
|
261
|
+
const now = new Date()
|
|
262
|
+
|
|
263
|
+
for (const event of timeline) {
|
|
264
|
+
// Look for pause events with reason "blocked"
|
|
265
|
+
if (event.type === 'pause' || event.type === 'session_paused') {
|
|
266
|
+
const e = event as { reason?: string; note?: string; task?: string; ts: string }
|
|
267
|
+
if (e.reason === 'blocked') {
|
|
268
|
+
const pauseDate = new Date(e.ts)
|
|
269
|
+
const daysBlocked = Math.floor((now.getTime() - pauseDate.getTime()) / (1000 * 60 * 60 * 24))
|
|
270
|
+
|
|
271
|
+
blockers.push({
|
|
272
|
+
task: e.task || 'Unknown task',
|
|
273
|
+
reason: e.note || 'Blocked',
|
|
274
|
+
since: e.ts,
|
|
275
|
+
daysBlocked
|
|
276
|
+
})
|
|
277
|
+
}
|
|
128
278
|
}
|
|
129
279
|
}
|
|
130
280
|
|
|
131
|
-
//
|
|
281
|
+
// Only return unresolved blockers (check if there's a resume after the pause)
|
|
282
|
+
// For now, return all - we'll refine this when we have resume tracking
|
|
283
|
+
return blockers.slice(0, 5) // Top 5 blockers
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Get project stats - cached per request
|
|
288
|
+
*
|
|
289
|
+
* MD-First Architecture: MD files are the source of truth.
|
|
290
|
+
* No JSON fallback - all data comes from MD.
|
|
291
|
+
*/
|
|
292
|
+
export const getStats = cache(async (projectId: string): Promise<StatsResult> => {
|
|
132
293
|
try {
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
294
|
+
const mdStats = await getMdStats(projectId)
|
|
295
|
+
|
|
296
|
+
// Check if we have meaningful data
|
|
297
|
+
const hasData = Boolean(
|
|
298
|
+
mdStats.currentTask ||
|
|
299
|
+
mdStats.queue.length > 0 ||
|
|
300
|
+
mdStats.shipped.length > 0 ||
|
|
301
|
+
mdStats.timeline.length > 0 ||
|
|
302
|
+
mdStats.summary.totalEvents > 0
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
if (hasData) {
|
|
306
|
+
// Calculate real metrics
|
|
307
|
+
const estimateAccuracy = calculateEstimateAccuracy(mdStats.sessions)
|
|
308
|
+
const blockers = extractBlockers(mdStats.timeline)
|
|
309
|
+
|
|
310
|
+
return {
|
|
311
|
+
...EMPTY_STATS_RESULT,
|
|
312
|
+
insights: {
|
|
313
|
+
healthScore: calculateHealthScoreV2(mdStats, estimateAccuracy, blockers),
|
|
314
|
+
estimateAccuracy,
|
|
315
|
+
blockers,
|
|
316
|
+
recommendations: generateRecommendations(mdStats, estimateAccuracy, blockers)
|
|
317
|
+
},
|
|
318
|
+
hasData: true,
|
|
319
|
+
isLegacy: true,
|
|
320
|
+
legacyStats: mdStats
|
|
321
|
+
}
|
|
144
322
|
}
|
|
145
323
|
} catch {
|
|
146
|
-
return
|
|
324
|
+
// MD parsing failed - return empty stats
|
|
147
325
|
}
|
|
326
|
+
|
|
327
|
+
return EMPTY_STATS_RESULT
|
|
148
328
|
})
|
|
149
329
|
|
|
330
|
+
/**
|
|
331
|
+
* Calculate health score V2 - based on real data
|
|
332
|
+
*
|
|
333
|
+
* Formula:
|
|
334
|
+
* - estimateAccuracy (25): % of tasks within ±20% of estimate
|
|
335
|
+
* - completionRate (25): tasks completed / tasks started (last 7 days)
|
|
336
|
+
* - noBlockers (25): penalize if blocked tasks exist
|
|
337
|
+
* - recentActivity (25): days active in last week
|
|
338
|
+
*/
|
|
339
|
+
function calculateHealthScoreV2(
|
|
340
|
+
stats: ProjectStats,
|
|
341
|
+
estimateAccuracy: number,
|
|
342
|
+
blockers: Blocker[]
|
|
343
|
+
): number {
|
|
344
|
+
const { timeline, sessions, currentTask } = stats
|
|
345
|
+
|
|
346
|
+
// Estimate accuracy score (0-25)
|
|
347
|
+
// If no estimates yet, give benefit of doubt (15/25)
|
|
348
|
+
const accuracyScore = estimateAccuracy > 0
|
|
349
|
+
? Math.round((estimateAccuracy / 100) * 25)
|
|
350
|
+
: 15
|
|
351
|
+
|
|
352
|
+
// Completion rate (0-25)
|
|
353
|
+
const recentSessions = sessions.slice(0, 7)
|
|
354
|
+
let tasksStarted = 0
|
|
355
|
+
let tasksCompleted = 0
|
|
356
|
+
for (const session of recentSessions) {
|
|
357
|
+
tasksStarted += session.tasksStarted
|
|
358
|
+
tasksCompleted += session.tasksCompleted
|
|
359
|
+
}
|
|
360
|
+
const completionRate = tasksStarted > 0 ? tasksCompleted / tasksStarted : 0
|
|
361
|
+
const completionScore = Math.round(Math.min(1, completionRate) * 25)
|
|
362
|
+
|
|
363
|
+
// No blockers score (0-25)
|
|
364
|
+
// Full points if no blockers, -5 per blocker, -10 per blocker > 3 days
|
|
365
|
+
let blockerPenalty = 0
|
|
366
|
+
for (const blocker of blockers) {
|
|
367
|
+
blockerPenalty += blocker.daysBlocked > 3 ? 10 : 5
|
|
368
|
+
}
|
|
369
|
+
const blockerScore = Math.max(0, 25 - blockerPenalty)
|
|
370
|
+
|
|
371
|
+
// Activity score (0-25)
|
|
372
|
+
// Count unique active days in last 7 days
|
|
373
|
+
const lastWeek = new Date()
|
|
374
|
+
lastWeek.setDate(lastWeek.getDate() - 7)
|
|
375
|
+
const recentDays = new Set(
|
|
376
|
+
timeline
|
|
377
|
+
.filter(e => new Date(e.ts) > lastWeek)
|
|
378
|
+
.map(e => e.ts.split('T')[0])
|
|
379
|
+
)
|
|
380
|
+
// Bonus for having current task
|
|
381
|
+
const activityBase = Math.min(7, recentDays.size) * 3 // up to 21
|
|
382
|
+
const currentTaskBonus = currentTask ? 4 : 0
|
|
383
|
+
const activityScore = Math.min(25, activityBase + currentTaskBonus)
|
|
384
|
+
|
|
385
|
+
return Math.min(100, accuracyScore + completionScore + blockerScore + activityScore)
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Legacy health score calculation (kept for fallback)
|
|
390
|
+
*/
|
|
391
|
+
function calculateHealthFromMd(stats: ProjectStats): number {
|
|
392
|
+
const { metrics, currentTask, queue, timeline } = stats
|
|
393
|
+
const velocity = metrics?.velocity?.tasksPerDay ?? 0
|
|
394
|
+
const hasCurrentTask = Boolean(currentTask)
|
|
395
|
+
const queueSize = queue?.length ?? 0
|
|
396
|
+
const recentActivity = timeline?.slice(0, 7).length ?? 0
|
|
397
|
+
|
|
398
|
+
const velocityScore = Math.min(30, velocity * 15)
|
|
399
|
+
const taskScore = hasCurrentTask ? 20 : 0
|
|
400
|
+
const queueScore = queueSize > 0 && queueSize < 15 ? 20 : queueSize === 0 ? 5 : 10
|
|
401
|
+
const activityScore = Math.min(30, recentActivity * 5)
|
|
402
|
+
|
|
403
|
+
return Math.min(100, Math.round(velocityScore + taskScore + queueScore + activityScore))
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Generate recommendations based on MD stats and insights
|
|
408
|
+
*/
|
|
409
|
+
function generateRecommendations(
|
|
410
|
+
stats: ProjectStats,
|
|
411
|
+
estimateAccuracy: number,
|
|
412
|
+
blockers: Blocker[]
|
|
413
|
+
): string[] {
|
|
414
|
+
const recommendations: string[] = []
|
|
415
|
+
|
|
416
|
+
// Blocker-based recommendations (highest priority)
|
|
417
|
+
if (blockers.length > 0) {
|
|
418
|
+
const oldestBlocker = blockers.reduce((a, b) => a.daysBlocked > b.daysBlocked ? a : b)
|
|
419
|
+
if (oldestBlocker.daysBlocked > 3) {
|
|
420
|
+
recommendations.push(`Blocker "${oldestBlocker.reason}" is ${oldestBlocker.daysBlocked} days old - needs attention`)
|
|
421
|
+
} else {
|
|
422
|
+
recommendations.push(`${blockers.length} blocked task(s) - review blockers`)
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// Estimate accuracy recommendations
|
|
427
|
+
if (estimateAccuracy > 0 && estimateAccuracy < 50) {
|
|
428
|
+
recommendations.push('Estimates often off - consider adding 30% buffer')
|
|
429
|
+
} else if (estimateAccuracy >= 80) {
|
|
430
|
+
recommendations.push('Great estimation accuracy - keep it up!')
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// Task state recommendations
|
|
434
|
+
if (!stats.currentTask) {
|
|
435
|
+
recommendations.push('Start a task with /p:now')
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
if (stats.queue.length === 0) {
|
|
439
|
+
recommendations.push('Add tasks to queue with /p:next')
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
if (stats.ideas.pending.length > 10) {
|
|
443
|
+
recommendations.push('Review and prioritize pending ideas')
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
if (stats.agents.length === 0) {
|
|
447
|
+
recommendations.push('Run /p:sync to generate agents')
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// Default positive message
|
|
451
|
+
if (recommendations.length === 0) {
|
|
452
|
+
recommendations.push('Keep shipping!')
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
return recommendations.slice(0, 4) // Max 4 recommendations
|
|
456
|
+
}
|
|
457
|
+
|
|
150
458
|
/**
|
|
151
459
|
* Calculate streak from metrics (pure function, no mutation)
|
|
152
460
|
*/
|
|
@@ -183,16 +491,6 @@ export function calculateStreak(metrics: MetricsJson | null): number {
|
|
|
183
491
|
return firstGapIndex === -1 ? days.length : firstGapIndex
|
|
184
492
|
}
|
|
185
493
|
|
|
186
|
-
/**
|
|
187
|
-
* Get health emoji based on score
|
|
188
|
-
*/
|
|
189
|
-
export function getHealthEmoji(score: number): string {
|
|
190
|
-
if (score >= 80) return '🔥'
|
|
191
|
-
if (score >= 60) return '💪'
|
|
192
|
-
if (score >= 40) return '👍'
|
|
193
|
-
if (score >= 20) return '🌱'
|
|
194
|
-
return '💤'
|
|
195
|
-
}
|
|
196
494
|
|
|
197
495
|
/**
|
|
198
496
|
* Get insight message based on stats
|
|
@@ -231,7 +529,7 @@ export function getWeeklyVelocityData(metrics: MetricsJson | null): number[] {
|
|
|
231
529
|
date.setDate(date.getDate() - (6 - i))
|
|
232
530
|
const dateStr = date.toISOString().split('T')[0]
|
|
233
531
|
|
|
234
|
-
return metrics.recentActivity.filter((e: { timestamp: string }) =>
|
|
532
|
+
return (metrics.recentActivity || []).filter((e: { timestamp: string }) =>
|
|
235
533
|
e.timestamp?.startsWith(dateStr)
|
|
236
534
|
).length
|
|
237
535
|
})
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/// <reference types="next" />
|
|
2
2
|
/// <reference types="next/image-types/global" />
|
|
3
|
-
import "./.next/types/routes.d.ts";
|
|
3
|
+
import "./.next/dev/types/routes.d.ts";
|
|
4
4
|
|
|
5
5
|
// NOTE: This file should not be edited
|
|
6
6
|
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
|
@@ -4,14 +4,11 @@
|
|
|
4
4
|
"private": true,
|
|
5
5
|
"scripts": {
|
|
6
6
|
"dev": "NODE_ENV=development PORT=9471 bun server.ts",
|
|
7
|
-
"dev:next": "bun next dev",
|
|
8
7
|
"build": "bun next build",
|
|
9
8
|
"start": "NODE_ENV=production PORT=9472 bun server.ts",
|
|
10
|
-
"start:prod": "NODE_ENV=production PORT=9472 bun server.ts",
|
|
11
9
|
"lint": "eslint"
|
|
12
10
|
},
|
|
13
11
|
"dependencies": {
|
|
14
|
-
"@ai-sdk/openai": "^2.0.80",
|
|
15
12
|
"@radix-ui/react-alert-dialog": "^1.1.15",
|
|
16
13
|
"@radix-ui/react-dialog": "^1.1.15",
|
|
17
14
|
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
|
@@ -24,7 +21,6 @@
|
|
|
24
21
|
"@xterm/addon-fit": "^0.10.0",
|
|
25
22
|
"@xterm/addon-web-links": "^0.11.0",
|
|
26
23
|
"@xterm/xterm": "^5.5.0",
|
|
27
|
-
"ai": "^5.0.108",
|
|
28
24
|
"class-variance-authority": "^0.7.1",
|
|
29
25
|
"clsx": "^2.1.1",
|
|
30
26
|
"lucide-react": "^0.556.0",
|