prjct-cli 0.13.2 → 0.15.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 +106 -0
- package/bin/prjct +10 -13
- package/core/agentic/memory-system/semantic-memories.ts +2 -1
- package/core/agentic/plan-mode/plan-mode.ts +2 -1
- package/core/agentic/prompt-builder.ts +22 -43
- package/core/agentic/services.ts +5 -5
- package/core/agentic/smart-context.ts +7 -2
- package/core/command-registry/core-commands.ts +54 -29
- package/core/command-registry/optional-commands.ts +64 -0
- package/core/command-registry/setup-commands.ts +18 -3
- package/core/commands/analysis.ts +21 -68
- package/core/commands/analytics.ts +247 -213
- package/core/commands/base.ts +1 -1
- package/core/commands/index.ts +41 -36
- package/core/commands/maintenance.ts +300 -31
- package/core/commands/planning.ts +233 -22
- package/core/commands/setup.ts +3 -8
- package/core/commands/shipping.ts +14 -18
- package/core/commands/types.ts +8 -6
- package/core/commands/workflow.ts +105 -100
- package/core/context/generator.ts +317 -0
- package/core/context-sync.ts +7 -350
- package/core/data/index.ts +13 -32
- package/core/data/md-ideas-manager.ts +155 -0
- package/core/data/md-queue-manager.ts +4 -3
- package/core/data/md-shipped-manager.ts +90 -0
- package/core/data/md-state-manager.ts +11 -7
- package/core/domain/agent-generator.ts +23 -63
- package/core/events/index.ts +143 -0
- package/core/index.ts +17 -14
- package/core/infrastructure/capability-installer.ts +13 -149
- package/core/infrastructure/migrator/project-scanner.ts +2 -1
- package/core/infrastructure/path-manager.ts +4 -6
- package/core/infrastructure/setup.ts +3 -0
- package/core/infrastructure/uuid-migration.ts +750 -0
- package/core/outcomes/recorder.ts +2 -1
- package/core/plugin/loader.ts +4 -7
- package/core/plugin/registry.ts +3 -3
- package/core/schemas/index.ts +23 -25
- package/core/schemas/state.ts +1 -0
- package/core/serializers/ideas-serializer.ts +187 -0
- package/core/serializers/index.ts +16 -0
- package/core/serializers/shipped-serializer.ts +108 -0
- package/core/session/utils.ts +3 -9
- package/core/storage/ideas-storage.ts +273 -0
- package/core/storage/index.ts +204 -0
- package/core/storage/queue-storage.ts +297 -0
- package/core/storage/shipped-storage.ts +223 -0
- package/core/storage/state-storage.ts +235 -0
- package/core/storage/storage-manager.ts +175 -0
- package/package.json +1 -1
- package/packages/web/app/api/projects/[id]/momentum/route.ts +257 -0
- package/packages/web/app/api/sessions/current/route.ts +132 -0
- package/packages/web/app/api/sessions/history/route.ts +96 -14
- package/packages/web/app/globals.css +5 -0
- package/packages/web/app/layout.tsx +2 -0
- package/packages/web/app/project/[id]/code/layout.tsx +18 -0
- package/packages/web/app/project/[id]/code/page.tsx +408 -0
- package/packages/web/app/project/[id]/page.tsx +359 -389
- package/packages/web/app/project/[id]/reports/page.tsx +59 -0
- package/packages/web/app/project/[id]/reports/print/page.tsx +58 -0
- package/packages/web/components/ActivityTimeline/ActivityTimeline.tsx +0 -1
- package/packages/web/components/AgentsCard/AgentsCard.tsx +64 -34
- package/packages/web/components/AgentsCard/AgentsCard.types.ts +1 -0
- package/packages/web/components/AppSidebar/AppSidebar.tsx +135 -11
- package/packages/web/components/BentoCard/BentoCard.constants.ts +3 -3
- package/packages/web/components/BentoCard/BentoCard.tsx +2 -1
- package/packages/web/components/BentoGrid/BentoGrid.tsx +2 -2
- package/packages/web/components/BlockersCard/BlockersCard.tsx +65 -57
- package/packages/web/components/BlockersCard/BlockersCard.types.ts +1 -0
- package/packages/web/components/CommandBar/CommandBar.tsx +67 -0
- package/packages/web/components/CommandBar/index.ts +1 -0
- package/packages/web/components/DashboardContent/DashboardContent.tsx +35 -5
- package/packages/web/components/DateGroup/DateGroup.tsx +1 -1
- package/packages/web/components/EmptyState/EmptyState.tsx +39 -21
- package/packages/web/components/EmptyState/EmptyState.types.ts +1 -0
- package/packages/web/components/EventRow/EventRow.tsx +4 -4
- package/packages/web/components/EventRow/EventRow.utils.ts +3 -3
- package/packages/web/components/HeroSection/HeroSection.tsx +52 -15
- package/packages/web/components/HeroSection/HeroSection.types.ts +4 -4
- package/packages/web/components/HeroSection/HeroSection.utils.ts +7 -3
- package/packages/web/components/IdeasCard/IdeasCard.tsx +94 -27
- package/packages/web/components/IdeasCard/IdeasCard.types.ts +1 -0
- package/packages/web/components/MasonryGrid/MasonryGrid.tsx +18 -0
- package/packages/web/components/MasonryGrid/index.ts +1 -0
- package/packages/web/components/MomentumWidget/MomentumWidget.tsx +119 -0
- package/packages/web/components/MomentumWidget/MomentumWidget.types.ts +16 -0
- package/packages/web/components/MomentumWidget/index.ts +2 -0
- package/packages/web/components/NowCard/NowCard.tsx +81 -56
- package/packages/web/components/NowCard/NowCard.types.ts +1 -0
- package/packages/web/components/PageHeader/PageHeader.tsx +24 -0
- package/packages/web/components/PageHeader/index.ts +1 -0
- package/packages/web/components/ProgressRing/ProgressRing.constants.ts +2 -2
- package/packages/web/components/ProjectAvatar/ProjectAvatar.tsx +2 -2
- package/packages/web/components/ProjectColorDot/ProjectColorDot.tsx +37 -0
- package/packages/web/components/ProjectColorDot/index.ts +1 -0
- package/packages/web/components/ProjectSelectorModal/ProjectSelectorModal.tsx +104 -0
- package/packages/web/components/ProjectSelectorModal/index.ts +1 -0
- package/packages/web/components/Providers/Providers.tsx +4 -1
- package/packages/web/components/QueueCard/QueueCard.tsx +78 -25
- package/packages/web/components/QueueCard/QueueCard.types.ts +1 -0
- package/packages/web/components/QueueCard/QueueCard.utils.ts +3 -3
- package/packages/web/components/RecoverCard/RecoverCard.tsx +72 -0
- package/packages/web/components/RecoverCard/RecoverCard.types.ts +16 -0
- package/packages/web/components/RecoverCard/index.ts +2 -0
- package/packages/web/components/RoadmapCard/RoadmapCard.tsx +101 -33
- package/packages/web/components/RoadmapCard/RoadmapCard.types.ts +1 -0
- package/packages/web/components/ShipsCard/ShipsCard.tsx +71 -28
- package/packages/web/components/ShipsCard/ShipsCard.types.ts +2 -0
- package/packages/web/components/SparklineChart/SparklineChart.tsx +20 -18
- package/packages/web/components/StatsMasonry/StatsMasonry.tsx +95 -0
- package/packages/web/components/StatsMasonry/index.ts +1 -0
- package/packages/web/components/StreakCard/StreakCard.tsx +37 -35
- package/packages/web/components/TasksCounter/TasksCounter.tsx +1 -1
- package/packages/web/components/TechStackBadges/TechStackBadges.tsx +12 -4
- package/packages/web/components/TerminalDock/DockToggleTab.tsx +29 -0
- package/packages/web/components/TerminalDock/TerminalDock.tsx +386 -0
- package/packages/web/components/TerminalDock/TerminalDockTab.tsx +130 -0
- package/packages/web/components/TerminalDock/TerminalTabBar.tsx +142 -0
- package/packages/web/components/TerminalDock/index.ts +2 -0
- package/packages/web/components/VelocityBadge/VelocityBadge.tsx +8 -3
- package/packages/web/components/VelocityCard/VelocityCard.tsx +49 -47
- package/packages/web/components/WeeklyReports/PrintableReport.tsx +259 -0
- package/packages/web/components/WeeklyReports/ReportPreviewCard.tsx +187 -0
- package/packages/web/components/WeeklyReports/WeekCalendar.tsx +288 -0
- package/packages/web/components/WeeklyReports/WeeklyReports.tsx +149 -0
- package/packages/web/components/WeeklyReports/index.ts +4 -0
- package/packages/web/components/WeeklySparkline/WeeklySparkline.tsx +16 -4
- package/packages/web/components/WeeklySparkline/WeeklySparkline.types.ts +1 -0
- package/packages/web/components/charts/SessionsChart.tsx +6 -3
- package/packages/web/components/ui/dialog.tsx +143 -0
- package/packages/web/components/ui/drawer.tsx +135 -0
- package/packages/web/components/ui/select.tsx +187 -0
- package/packages/web/context/GlobalTerminalContext.tsx +538 -0
- package/packages/web/lib/commands.ts +81 -0
- package/packages/web/lib/generate-week-report.ts +285 -0
- package/packages/web/lib/parse-prjct-files.ts +56 -55
- package/packages/web/lib/project-colors.ts +58 -0
- package/packages/web/lib/projects.ts +58 -5
- package/packages/web/lib/services/projects.server.ts +11 -1
- package/packages/web/next-env.d.ts +1 -1
- package/packages/web/package.json +5 -1
- package/templates/commands/analyze.md +39 -3
- package/templates/commands/ask.md +58 -3
- package/templates/commands/bug.md +117 -26
- package/templates/commands/dash.md +95 -158
- package/templates/commands/done.md +130 -148
- package/templates/commands/feature.md +125 -103
- package/templates/commands/git.md +18 -3
- package/templates/commands/idea.md +121 -38
- package/templates/commands/init.md +124 -20
- package/templates/commands/migrate-all.md +63 -28
- package/templates/commands/migrate.md +140 -0
- package/templates/commands/next.md +115 -5
- package/templates/commands/now.md +146 -82
- package/templates/commands/pause.md +89 -74
- package/templates/commands/redo.md +6 -4
- package/templates/commands/resume.md +141 -59
- package/templates/commands/ship.md +103 -231
- package/templates/commands/spec.md +98 -8
- package/templates/commands/suggest.md +22 -2
- package/templates/commands/sync.md +192 -203
- package/templates/commands/undo.md +6 -4
- package/core/data/agents-manager.ts +0 -76
- package/core/data/analysis-manager.ts +0 -83
- package/core/data/base-manager.ts +0 -156
- package/core/data/ideas-manager.ts +0 -81
- package/core/data/outcomes-manager.ts +0 -96
- package/core/data/project-manager.ts +0 -75
- package/core/data/roadmap-manager.ts +0 -118
- package/core/data/shipped-manager.ts +0 -65
- package/core/data/state-manager.ts +0 -214
- package/core/state/index.ts +0 -25
- package/core/state/manager.ts +0 -376
- package/core/state/types.ts +0 -185
- package/core/utils/project-capabilities.ts +0 -156
- package/core/view-generator.ts +0 -536
- package/packages/web/app/project/[id]/stats/loading.tsx +0 -43
- package/packages/web/app/project/[id]/stats/page.tsx +0 -253
- package/templates/agent-assignment.md +0 -72
- package/templates/analysis/project-analysis.md +0 -78
- package/templates/checklists/accessibility.md +0 -33
- package/templates/commands/build.md +0 -17
- package/templates/commands/decision.md +0 -226
- package/templates/commands/fix.md +0 -79
- package/templates/commands/help.md +0 -61
- package/templates/commands/progress.md +0 -14
- package/templates/commands/recap.md +0 -14
- package/templates/commands/roadmap.md +0 -52
- package/templates/commands/status.md +0 -17
- package/templates/commands/task.md +0 -63
- package/templates/commands/work.md +0 -44
- package/templates/commands/workflow.md +0 -12
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import type { StatsResult } from './services/stats.server'
|
|
2
|
+
|
|
3
|
+
export interface ShippedItem {
|
|
4
|
+
name: string
|
|
5
|
+
date: string
|
|
6
|
+
version?: string
|
|
7
|
+
type?: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface WeekData {
|
|
11
|
+
year: number
|
|
12
|
+
week: number
|
|
13
|
+
startDate: Date
|
|
14
|
+
endDate: Date
|
|
15
|
+
shipped: ShippedItem[]
|
|
16
|
+
tasksCompleted: number
|
|
17
|
+
bugsFixed: number
|
|
18
|
+
syncs: number
|
|
19
|
+
activeDays: number
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Get ISO week number for a date
|
|
24
|
+
*/
|
|
25
|
+
export function getWeekNumber(date: Date): number {
|
|
26
|
+
const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()))
|
|
27
|
+
const dayNum = d.getUTCDay() || 7
|
|
28
|
+
d.setUTCDate(d.getUTCDate() + 4 - dayNum)
|
|
29
|
+
const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1))
|
|
30
|
+
return Math.ceil((((d.getTime() - yearStart.getTime()) / 86400000) + 1) / 7)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Get current year and week
|
|
35
|
+
*/
|
|
36
|
+
export function getCurrentYearWeek(): { year: number; week: number } {
|
|
37
|
+
const now = new Date()
|
|
38
|
+
return {
|
|
39
|
+
year: now.getFullYear(),
|
|
40
|
+
week: getWeekNumber(now)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Get start and end dates for a given ISO week
|
|
46
|
+
*/
|
|
47
|
+
export function getWeekDateRange(year: number, week: number): { start: Date; end: Date } {
|
|
48
|
+
// Find January 4th of the year (always in week 1)
|
|
49
|
+
const jan4 = new Date(year, 0, 4)
|
|
50
|
+
const dayOfWeek = jan4.getDay() || 7
|
|
51
|
+
|
|
52
|
+
// Find Monday of week 1
|
|
53
|
+
const week1Monday = new Date(jan4)
|
|
54
|
+
week1Monday.setDate(jan4.getDate() - dayOfWeek + 1)
|
|
55
|
+
|
|
56
|
+
// Calculate target week's Monday
|
|
57
|
+
const targetMonday = new Date(week1Monday)
|
|
58
|
+
targetMonday.setDate(week1Monday.getDate() + (week - 1) * 7)
|
|
59
|
+
|
|
60
|
+
// Calculate Sunday
|
|
61
|
+
const targetSunday = new Date(targetMonday)
|
|
62
|
+
targetSunday.setDate(targetMonday.getDate() + 6)
|
|
63
|
+
|
|
64
|
+
return { start: targetMonday, end: targetSunday }
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Format date range as string
|
|
69
|
+
*/
|
|
70
|
+
export function formatDateRange(start: Date, end: Date): string {
|
|
71
|
+
const startMonth = start.toLocaleDateString('en-US', { month: 'short' })
|
|
72
|
+
const endMonth = end.toLocaleDateString('en-US', { month: 'short' })
|
|
73
|
+
|
|
74
|
+
if (startMonth === endMonth) {
|
|
75
|
+
return `${startMonth} ${start.getDate()}-${end.getDate()}`
|
|
76
|
+
}
|
|
77
|
+
return `${startMonth} ${start.getDate()} - ${endMonth} ${end.getDate()}`
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Check if a date falls within a week
|
|
82
|
+
*/
|
|
83
|
+
function isDateInWeek(dateStr: string, weekStart: Date, weekEnd: Date): boolean {
|
|
84
|
+
const date = new Date(dateStr)
|
|
85
|
+
return date >= weekStart && date <= weekEnd
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Filter stats data by week
|
|
90
|
+
*/
|
|
91
|
+
export function filterDataByWeek(
|
|
92
|
+
stats: StatsResult,
|
|
93
|
+
year: number,
|
|
94
|
+
week: number
|
|
95
|
+
): WeekData {
|
|
96
|
+
const { start, end } = getWeekDateRange(year, week)
|
|
97
|
+
|
|
98
|
+
// 1. Shipped features from legacyStats.shipped (parsed from shipped.md)
|
|
99
|
+
const shipped: ShippedItem[] = (stats.legacyStats?.shipped ?? [])
|
|
100
|
+
.filter(item => {
|
|
101
|
+
if (!item.date) return false
|
|
102
|
+
return isDateInWeek(item.date, start, end)
|
|
103
|
+
})
|
|
104
|
+
.map(item => ({
|
|
105
|
+
name: item.name,
|
|
106
|
+
date: item.date!,
|
|
107
|
+
version: item.version,
|
|
108
|
+
type: item.type
|
|
109
|
+
}))
|
|
110
|
+
|
|
111
|
+
// 2. Timeline events from legacyStats.timeline (parsed from context.jsonl)
|
|
112
|
+
const timeline = stats.legacyStats?.timeline ?? []
|
|
113
|
+
const weekEvents = timeline.filter(e => {
|
|
114
|
+
if (!e.ts) return false
|
|
115
|
+
return isDateInWeek(e.ts, start, end)
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
// Count tasks completed
|
|
119
|
+
const tasksCompleted = weekEvents.filter(e =>
|
|
120
|
+
e.type === 'task_complete' ||
|
|
121
|
+
e.type === 'task_completed' ||
|
|
122
|
+
e.type === 'session_completed'
|
|
123
|
+
).length
|
|
124
|
+
|
|
125
|
+
// Count bugs fixed
|
|
126
|
+
const bugsFixed = weekEvents.filter(e =>
|
|
127
|
+
e.type === 'bug_fix' ||
|
|
128
|
+
e.type === 'bug_reported'
|
|
129
|
+
).length
|
|
130
|
+
|
|
131
|
+
// Count syncs
|
|
132
|
+
const syncs = weekEvents.filter(e => e.type === 'sync').length
|
|
133
|
+
|
|
134
|
+
// 3. Also aggregate from sessions
|
|
135
|
+
const sessions = stats.legacyStats?.sessions ?? []
|
|
136
|
+
let sessionTasksCompleted = 0
|
|
137
|
+
let sessionFeatures = 0
|
|
138
|
+
const activeDaysSet = new Set<string>()
|
|
139
|
+
|
|
140
|
+
for (const session of sessions) {
|
|
141
|
+
if (session.date && isDateInWeek(session.date, start, end)) {
|
|
142
|
+
activeDaysSet.add(session.date)
|
|
143
|
+
sessionTasksCompleted += session.tasksCompleted || 0
|
|
144
|
+
sessionFeatures += session.featuresShipped || 0
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Add active days from timeline
|
|
149
|
+
for (const e of weekEvents) {
|
|
150
|
+
if (e.ts) {
|
|
151
|
+
activeDaysSet.add(e.ts.split('T')[0])
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return {
|
|
156
|
+
year,
|
|
157
|
+
week,
|
|
158
|
+
startDate: start,
|
|
159
|
+
endDate: end,
|
|
160
|
+
shipped,
|
|
161
|
+
tasksCompleted: tasksCompleted + sessionTasksCompleted,
|
|
162
|
+
bugsFixed,
|
|
163
|
+
syncs,
|
|
164
|
+
activeDays: activeDaysSet.size
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Generate plain text report for WhatsApp/email - client friendly
|
|
170
|
+
*/
|
|
171
|
+
export function generateReportText(
|
|
172
|
+
weekData: WeekData | WeekData[],
|
|
173
|
+
projectName: string
|
|
174
|
+
): string {
|
|
175
|
+
const weeks = Array.isArray(weekData) ? weekData : [weekData]
|
|
176
|
+
|
|
177
|
+
if (weeks.length === 0) {
|
|
178
|
+
return 'No weeks selected'
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const lines: string[] = []
|
|
182
|
+
|
|
183
|
+
// Header - clean and professional
|
|
184
|
+
lines.push(`*${projectName}*`)
|
|
185
|
+
|
|
186
|
+
// Date range - human readable
|
|
187
|
+
if (weeks.length === 1) {
|
|
188
|
+
const w = weeks[0]
|
|
189
|
+
lines.push(`Weekly Report: ${formatDateRange(w.startDate, w.endDate)}`)
|
|
190
|
+
} else {
|
|
191
|
+
const firstWeek = weeks[0]
|
|
192
|
+
const lastWeek = weeks[weeks.length - 1]
|
|
193
|
+
lines.push(`Report: ${formatDateRange(firstWeek.startDate, lastWeek.endDate)}`)
|
|
194
|
+
}
|
|
195
|
+
lines.push('')
|
|
196
|
+
|
|
197
|
+
// Aggregate data
|
|
198
|
+
const allShipped = weeks.flatMap(w => w.shipped)
|
|
199
|
+
// Deduplicate by name, keeping first occurrence (with version info)
|
|
200
|
+
const uniqueShipsMap = new Map<string, ShippedItem>()
|
|
201
|
+
for (const ship of allShipped) {
|
|
202
|
+
if (!uniqueShipsMap.has(ship.name)) {
|
|
203
|
+
uniqueShipsMap.set(ship.name, ship)
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
// Sort by date descending (most recent first)
|
|
207
|
+
const uniqueShips = Array.from(uniqueShipsMap.values())
|
|
208
|
+
.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime())
|
|
209
|
+
|
|
210
|
+
// Group by date
|
|
211
|
+
const shipsByDate = new Map<string, ShippedItem[]>()
|
|
212
|
+
for (const ship of uniqueShips) {
|
|
213
|
+
if (!shipsByDate.has(ship.date)) {
|
|
214
|
+
shipsByDate.set(ship.date, [])
|
|
215
|
+
}
|
|
216
|
+
shipsByDate.get(ship.date)!.push(ship)
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// What we delivered - grouped by date
|
|
220
|
+
if (uniqueShips.length > 0) {
|
|
221
|
+
lines.push('*Shipped:*')
|
|
222
|
+
for (const [date, ships] of shipsByDate.entries()) {
|
|
223
|
+
const dateStr = new Date(date).toLocaleDateString('en-US', {
|
|
224
|
+
weekday: 'short',
|
|
225
|
+
day: 'numeric',
|
|
226
|
+
month: 'short'
|
|
227
|
+
})
|
|
228
|
+
lines.push(`_${dateStr}_`)
|
|
229
|
+
for (const ship of ships) {
|
|
230
|
+
const versionStr = ship.version ? ` (${ship.version})` : ''
|
|
231
|
+
lines.push(`• ${ship.name}${versionStr}`)
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
lines.push('')
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Aggregate progress metrics
|
|
238
|
+
const totalTasks = weeks.reduce((sum, w) => sum + w.tasksCompleted, 0)
|
|
239
|
+
const totalBugs = weeks.reduce((sum, w) => sum + w.bugsFixed, 0)
|
|
240
|
+
const totalDays = weeks.reduce((sum, w) => sum + w.activeDays, 0)
|
|
241
|
+
|
|
242
|
+
// Progress section
|
|
243
|
+
if (totalTasks > 0 || totalBugs > 0 || totalDays > 0) {
|
|
244
|
+
lines.push('*Progress:*')
|
|
245
|
+
if (totalTasks > 0) {
|
|
246
|
+
lines.push(`• ${totalTasks} task${totalTasks !== 1 ? 's' : ''} completed`)
|
|
247
|
+
}
|
|
248
|
+
if (totalBugs > 0) {
|
|
249
|
+
lines.push(`• ${totalBugs} bug${totalBugs !== 1 ? 's' : ''} fixed`)
|
|
250
|
+
}
|
|
251
|
+
if (totalDays > 0) {
|
|
252
|
+
lines.push(`• ${totalDays} active day${totalDays !== 1 ? 's' : ''}`)
|
|
253
|
+
}
|
|
254
|
+
lines.push('')
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// If nothing happened, be honest
|
|
258
|
+
if (uniqueShips.length === 0 && totalTasks === 0 && totalBugs === 0) {
|
|
259
|
+
lines.push('_In progress, no deliveries this week._')
|
|
260
|
+
lines.push('')
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Optional: Next steps placeholder
|
|
264
|
+
lines.push('*Next:*')
|
|
265
|
+
lines.push('• [To be defined]')
|
|
266
|
+
|
|
267
|
+
return lines.join('\n')
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Get activity level for a week (for calendar indicator)
|
|
272
|
+
*/
|
|
273
|
+
export function getWeekActivityLevel(
|
|
274
|
+
stats: StatsResult,
|
|
275
|
+
year: number,
|
|
276
|
+
week: number
|
|
277
|
+
): 'none' | 'low' | 'medium' | 'high' {
|
|
278
|
+
const data = filterDataByWeek(stats, year, week)
|
|
279
|
+
const total = data.shipped.length + data.tasksCompleted + data.bugsFixed + data.syncs
|
|
280
|
+
|
|
281
|
+
if (total === 0) return 'none'
|
|
282
|
+
if (total <= 3) return 'low'
|
|
283
|
+
if (total <= 10) return 'medium'
|
|
284
|
+
return 'high'
|
|
285
|
+
}
|
|
@@ -463,68 +463,69 @@ export function parseShipped(content: string): ShippedFeature[] {
|
|
|
463
463
|
const features: ShippedFeature[] = []
|
|
464
464
|
if (!content) return features
|
|
465
465
|
|
|
466
|
-
//
|
|
467
|
-
const
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
const dateMatch = headerLine.match(/^(\d{4}-\d{2}-\d{2})/)
|
|
476
|
-
|
|
477
|
-
if (!versionMatch && !dateMatch) continue
|
|
478
|
-
|
|
479
|
-
const feature: ShippedFeature = {
|
|
480
|
-
date: dateMatch ? dateMatch[1] : new Date().toISOString().split('T')[0],
|
|
481
|
-
name: versionMatch ? versionMatch[2].trim() : '',
|
|
482
|
-
version: versionMatch ? versionMatch[1] : undefined
|
|
466
|
+
// Try ISO date format first: ## 2025-01-01
|
|
467
|
+
const isoDateSections = content.split(/^##\s+(\d{4}-\d{2}-\d{2})/m)
|
|
468
|
+
|
|
469
|
+
if (isoDateSections.length > 1) {
|
|
470
|
+
// Process pairs: [before, date1, content1, date2, content2, ...]
|
|
471
|
+
for (let i = 1; i < isoDateSections.length; i += 2) {
|
|
472
|
+
const sectionDate = isoDateSections[i]
|
|
473
|
+
const sectionContent = isoDateSections[i + 1] || ''
|
|
474
|
+
parseShippedSection(sectionContent, sectionDate, features)
|
|
483
475
|
}
|
|
484
|
-
|
|
485
|
-
//
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
const addedMatch = line.match(/\+(\d+)/i)
|
|
494
|
-
const removedMatch = line.match(/-(\d+)/i)
|
|
495
|
-
|
|
496
|
-
if (typeMatch) feature.type = typeMatch[1]
|
|
497
|
-
if (agentMatch) feature.agent = agentMatch[1]
|
|
498
|
-
if (timeMatch) feature.time = timeMatch[1].trim()
|
|
499
|
-
if (commitMatch) feature.commit = commitMatch[1]
|
|
500
|
-
if (impactMatch) feature.impact = impactMatch[1]
|
|
501
|
-
if (filesMatch) feature.filesChanged = parseInt(filesMatch[1])
|
|
502
|
-
if (addedMatch) feature.linesAdded = parseInt(addedMatch[1])
|
|
503
|
-
if (removedMatch) feature.linesRemoved = parseInt(removedMatch[1])
|
|
504
|
-
|
|
505
|
-
// Root cause and solution for fixes
|
|
506
|
-
if (line.includes('Root Cause')) {
|
|
507
|
-
feature.rootCause = line.replace(/.*Root Cause\*?\*?:\s*/i, '').trim()
|
|
508
|
-
}
|
|
509
|
-
if (line.includes('Solution')) {
|
|
510
|
-
feature.solution = line.replace(/.*Solution\*?\*?:\s*/i, '').trim()
|
|
476
|
+
} else {
|
|
477
|
+
// Try month format: ## November 2025 or ## December 2024
|
|
478
|
+
const monthDateSections = content.split(/^##\s+(\w+\s+\d{4})/m)
|
|
479
|
+
|
|
480
|
+
if (monthDateSections.length > 1) {
|
|
481
|
+
for (let i = 1; i < monthDateSections.length; i += 2) {
|
|
482
|
+
const sectionDate = monthDateSections[i]
|
|
483
|
+
const sectionContent = monthDateSections[i + 1] || ''
|
|
484
|
+
parseShippedSection(sectionContent, sectionDate, features)
|
|
511
485
|
}
|
|
486
|
+
} else {
|
|
487
|
+
// No date headers - parse entire content with inline dates
|
|
488
|
+
parseShippedSection(content, '', features)
|
|
512
489
|
}
|
|
490
|
+
}
|
|
513
491
|
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
const featureLineMatch = section.match(/\*\*([^*]+)\*\*/m)
|
|
517
|
-
if (featureLineMatch) {
|
|
518
|
-
feature.name = featureLineMatch[1].trim()
|
|
519
|
-
}
|
|
520
|
-
}
|
|
492
|
+
return features.slice(0, 30) // Last 30
|
|
493
|
+
}
|
|
521
494
|
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
495
|
+
function parseShippedSection(sectionContent: string, sectionDate: string, features: ShippedFeature[]) {
|
|
496
|
+
// Parse bullet points: - **Name** (version) or - **Name** - 2025-01-01
|
|
497
|
+
const bulletRegex = /^-\s+\*\*(.+?)\*\*(?:\s*\(([^)]+)\))?(?:\s*-\s*(\d{4}-\d{2}-\d{2}))?/gm
|
|
498
|
+
let match
|
|
499
|
+
while ((match = bulletRegex.exec(sectionContent)) !== null) {
|
|
500
|
+
const name = match[1].trim()
|
|
501
|
+
const versionOrNote = match[2]?.trim()
|
|
502
|
+
const inlineDate = match[3]
|
|
503
|
+
|
|
504
|
+
// Extract version if it starts with 'v'
|
|
505
|
+
const version = versionOrNote?.match(/^v[\d.]+/) ? versionOrNote : undefined
|
|
506
|
+
|
|
507
|
+
features.push({
|
|
508
|
+
date: inlineDate || sectionDate || new Date().toISOString().split('T')[0],
|
|
509
|
+
name,
|
|
510
|
+
version
|
|
511
|
+
})
|
|
525
512
|
}
|
|
526
513
|
|
|
527
|
-
|
|
514
|
+
// Also check for ### Feature Name headers (alternative format)
|
|
515
|
+
const headerRegex = /^###\s+(.+?)(?:\s+(v[\d.]+))?\s*$/gm
|
|
516
|
+
while ((match = headerRegex.exec(sectionContent)) !== null) {
|
|
517
|
+
const name = match[1].trim()
|
|
518
|
+
const version = match[2]
|
|
519
|
+
|
|
520
|
+
// Avoid duplicates if same feature in both formats
|
|
521
|
+
if (!features.some(f => f.date === sectionDate && f.name === name)) {
|
|
522
|
+
features.push({
|
|
523
|
+
date: sectionDate || new Date().toISOString().split('T')[0],
|
|
524
|
+
name,
|
|
525
|
+
version
|
|
526
|
+
})
|
|
527
|
+
}
|
|
528
|
+
}
|
|
528
529
|
}
|
|
529
530
|
|
|
530
531
|
// Parse ideas.md - Full idea structure
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project Colors - Consistent color generation based on projectId
|
|
3
|
+
*
|
|
4
|
+
* Uses a hash of the projectId to generate a consistent color
|
|
5
|
+
* that matches between UI elements and browser tab titles.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// Color palette - visually distinct colors that work well in both
|
|
9
|
+
// Tailwind classes and as emojis
|
|
10
|
+
export const PROJECT_COLORS = [
|
|
11
|
+
{ name: 'red', emoji: '🔴', bg: 'bg-red-500', text: 'text-red-500' },
|
|
12
|
+
{ name: 'orange', emoji: '🟠', bg: 'bg-orange-500', text: 'text-orange-500' },
|
|
13
|
+
{ name: 'yellow', emoji: '🟡', bg: 'bg-yellow-500', text: 'text-yellow-500' },
|
|
14
|
+
{ name: 'green', emoji: '🟢', bg: 'bg-green-500', text: 'text-green-500' },
|
|
15
|
+
{ name: 'blue', emoji: '🔵', bg: 'bg-blue-500', text: 'text-blue-500' },
|
|
16
|
+
{ name: 'purple', emoji: '🟣', bg: 'bg-purple-500', text: 'text-purple-500' },
|
|
17
|
+
{ name: 'brown', emoji: '🟤', bg: 'bg-amber-700', text: 'text-amber-700' },
|
|
18
|
+
] as const
|
|
19
|
+
|
|
20
|
+
export type ProjectColor = typeof PROJECT_COLORS[number]
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Simple hash function for strings
|
|
24
|
+
*/
|
|
25
|
+
function hashString(str: string): number {
|
|
26
|
+
let hash = 0
|
|
27
|
+
for (let i = 0; i < str.length; i++) {
|
|
28
|
+
const char = str.charCodeAt(i)
|
|
29
|
+
hash = ((hash << 5) - hash) + char
|
|
30
|
+
hash = hash & hash // Convert to 32bit integer
|
|
31
|
+
}
|
|
32
|
+
return Math.abs(hash)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Get a consistent color for a project based on its ID
|
|
37
|
+
*/
|
|
38
|
+
export function getProjectColor(projectId: string): ProjectColor {
|
|
39
|
+
const hash = hashString(projectId)
|
|
40
|
+
const index = hash % PROJECT_COLORS.length
|
|
41
|
+
return PROJECT_COLORS[index]
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Get just the emoji for a project (for use in titles)
|
|
46
|
+
* Returns a neutral symbol instead of colored circles
|
|
47
|
+
*/
|
|
48
|
+
export function getProjectEmoji(projectId: string): string {
|
|
49
|
+
return '▸'
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Get the Tailwind background class for a project
|
|
54
|
+
* Returns neutral color - projects no longer have color-coded backgrounds
|
|
55
|
+
*/
|
|
56
|
+
export function getProjectBgClass(projectId: string): string {
|
|
57
|
+
return 'bg-muted'
|
|
58
|
+
}
|
|
@@ -7,6 +7,7 @@ import { join, dirname } from 'path'
|
|
|
7
7
|
import { homedir } from 'os'
|
|
8
8
|
import { exec } from 'child_process'
|
|
9
9
|
import { promisify } from 'util'
|
|
10
|
+
import { listSessions } from './pty'
|
|
10
11
|
|
|
11
12
|
const execAsync = promisify(exec)
|
|
12
13
|
|
|
@@ -154,11 +155,16 @@ export async function getProjects() {
|
|
|
154
155
|
let hasActiveSession = false
|
|
155
156
|
let lastActivity: string | null = null
|
|
156
157
|
|
|
157
|
-
//
|
|
158
|
+
// Check for real PTY sessions (actual Claude sessions in memory)
|
|
159
|
+
try {
|
|
160
|
+
const activeSessions = listSessions()
|
|
161
|
+
hasActiveSession = activeSessions.some(s => s.projectDir === repoPath)
|
|
162
|
+
} catch {}
|
|
163
|
+
|
|
164
|
+
// Try current session for lastActivity only
|
|
158
165
|
try {
|
|
159
166
|
const sessionPath = join(storagePath, 'sessions', 'current.json')
|
|
160
167
|
const sessionData = JSON.parse(await fs.readFile(sessionPath, 'utf-8'))
|
|
161
|
-
hasActiveSession = sessionData.status === 'active'
|
|
162
168
|
lastActivity = sessionData.startedAt || sessionData.updatedAt
|
|
163
169
|
} catch {}
|
|
164
170
|
|
|
@@ -188,9 +194,10 @@ export async function getProjects() {
|
|
|
188
194
|
}
|
|
189
195
|
}
|
|
190
196
|
|
|
191
|
-
// Count ideas and
|
|
197
|
+
// Count ideas, next tasks, and shipped items
|
|
192
198
|
let ideasCount = 0
|
|
193
199
|
let nextTasksCount = 0
|
|
200
|
+
let shippedCount = 0
|
|
194
201
|
try {
|
|
195
202
|
const ideasContent = await fs.readFile(join(storagePath, 'planning', 'ideas.md'), 'utf-8')
|
|
196
203
|
ideasCount = (ideasContent.match(/^- /gm) || []).length
|
|
@@ -199,6 +206,13 @@ export async function getProjects() {
|
|
|
199
206
|
const nextContent = await fs.readFile(join(storagePath, 'core', 'next.md'), 'utf-8')
|
|
200
207
|
nextTasksCount = (nextContent.match(/^- /gm) || []).length
|
|
201
208
|
} catch {}
|
|
209
|
+
try {
|
|
210
|
+
const shippedContent = await fs.readFile(join(storagePath, 'progress', 'shipped.md'), 'utf-8')
|
|
211
|
+
// Count shipped items: either "- **Name**" or "### Name" format
|
|
212
|
+
const bulletItems = (shippedContent.match(/^- \*\*/gm) || []).length
|
|
213
|
+
const headingItems = (shippedContent.match(/^### /gm) || []).length
|
|
214
|
+
shippedCount = bulletItems + headingItems
|
|
215
|
+
} catch {}
|
|
202
216
|
|
|
203
217
|
// Find favicon/icon in project repo
|
|
204
218
|
let iconPath: string | null = null
|
|
@@ -237,6 +251,7 @@ export async function getProjects() {
|
|
|
237
251
|
lastActivity,
|
|
238
252
|
ideasCount,
|
|
239
253
|
nextTasksCount,
|
|
254
|
+
shippedCount,
|
|
240
255
|
techStack,
|
|
241
256
|
iconPath
|
|
242
257
|
})
|
|
@@ -318,6 +333,23 @@ export async function getProject(projectId: string) {
|
|
|
318
333
|
currentTask = await fs.readFile(nowPath, 'utf-8')
|
|
319
334
|
} catch {}
|
|
320
335
|
|
|
336
|
+
// Count shipped and queue items (DRY - same logic as getProjects)
|
|
337
|
+
let shippedCount = 0
|
|
338
|
+
let nextTasksCount = 0
|
|
339
|
+
try {
|
|
340
|
+
const shippedContent = await fs.readFile(join(storagePath, 'progress', 'shipped.md'), 'utf-8')
|
|
341
|
+
const bulletItems = (shippedContent.match(/^- \*\*/gm) || []).length
|
|
342
|
+
const headingItems = (shippedContent.match(/^### /gm) || []).length
|
|
343
|
+
shippedCount = bulletItems + headingItems
|
|
344
|
+
} catch {}
|
|
345
|
+
try {
|
|
346
|
+
const nextContent = await fs.readFile(join(storagePath, 'core', 'next.md'), 'utf-8')
|
|
347
|
+
nextTasksCount = (nextContent.match(/^- /gm) || []).length
|
|
348
|
+
} catch {}
|
|
349
|
+
|
|
350
|
+
const total = shippedCount + nextTasksCount
|
|
351
|
+
const completionRate = total > 0 ? Math.round((shippedCount / total) * 100) : 0
|
|
352
|
+
|
|
321
353
|
// Fallback: Extract stats from claudeMd Quick Reference table if not from project.json
|
|
322
354
|
if (!version) {
|
|
323
355
|
const versionMatch = claudeMd.match(/\*\*Version\*\*\s*\|\s*([^\n|]+)/)
|
|
@@ -380,7 +412,11 @@ export async function getProject(projectId: string) {
|
|
|
380
412
|
filesCount,
|
|
381
413
|
commitsCount,
|
|
382
414
|
techStack,
|
|
383
|
-
iconPath
|
|
415
|
+
iconPath,
|
|
416
|
+
// Counts (DRY - same source as dashboard)
|
|
417
|
+
shippedCount,
|
|
418
|
+
nextTasksCount,
|
|
419
|
+
completionRate
|
|
384
420
|
}
|
|
385
421
|
} catch {
|
|
386
422
|
return null
|
|
@@ -424,11 +460,28 @@ export async function getProjectStatus(projectId: string) {
|
|
|
424
460
|
const projectPath = join(GLOBAL_STORAGE, projectId)
|
|
425
461
|
|
|
426
462
|
let session = null
|
|
463
|
+
let repoPath: string | null = null
|
|
427
464
|
try {
|
|
428
465
|
const sessionPath = join(projectPath, 'sessions', 'current.json')
|
|
429
466
|
session = JSON.parse(await fs.readFile(sessionPath, 'utf-8'))
|
|
430
467
|
} catch {}
|
|
431
468
|
|
|
469
|
+
// Get repoPath from project.json for PTY session check
|
|
470
|
+
try {
|
|
471
|
+
const projectJsonPath = join(projectPath, 'project.json')
|
|
472
|
+
const projectJson = JSON.parse(await fs.readFile(projectJsonPath, 'utf-8'))
|
|
473
|
+
repoPath = projectJson.repoPath || null
|
|
474
|
+
} catch {}
|
|
475
|
+
|
|
476
|
+
// Check for real PTY sessions
|
|
477
|
+
let hasActiveSession = false
|
|
478
|
+
if (repoPath) {
|
|
479
|
+
try {
|
|
480
|
+
const activeSessions = listSessions()
|
|
481
|
+
hasActiveSession = activeSessions.some(s => s.projectDir === repoPath)
|
|
482
|
+
} catch {}
|
|
483
|
+
}
|
|
484
|
+
|
|
432
485
|
let ideas: string[] = []
|
|
433
486
|
try {
|
|
434
487
|
const ideasPath = join(projectPath, 'planning', 'ideas.md')
|
|
@@ -446,7 +499,7 @@ export async function getProjectStatus(projectId: string) {
|
|
|
446
499
|
return {
|
|
447
500
|
projectId,
|
|
448
501
|
session,
|
|
449
|
-
hasActiveSession
|
|
502
|
+
hasActiveSession,
|
|
450
503
|
ideas,
|
|
451
504
|
nextTasks
|
|
452
505
|
}
|
|
@@ -19,6 +19,11 @@ export interface ProjectJson {
|
|
|
19
19
|
commitCount: number
|
|
20
20
|
createdAt: string
|
|
21
21
|
lastSync: string
|
|
22
|
+
version?: string | null
|
|
23
|
+
// Counts - DRY: single source of truth for dashboard/detail
|
|
24
|
+
shippedCount: number
|
|
25
|
+
nextTasksCount: number
|
|
26
|
+
completionRate: number
|
|
22
27
|
}
|
|
23
28
|
|
|
24
29
|
/**
|
|
@@ -38,7 +43,12 @@ export const getProject = cache(async (projectId: string): Promise<ProjectJson |
|
|
|
38
43
|
fileCount: project.filesCount ? parseInt(project.filesCount) : 0,
|
|
39
44
|
commitCount: project.commitsCount ? parseInt(project.commitsCount) : 0,
|
|
40
45
|
createdAt: new Date().toISOString(),
|
|
41
|
-
lastSync: new Date().toISOString()
|
|
46
|
+
lastSync: new Date().toISOString(),
|
|
47
|
+
version: project.version || null,
|
|
48
|
+
// DRY: Pass through counts from lib/projects.ts (single source of truth)
|
|
49
|
+
shippedCount: project.shippedCount ?? 0,
|
|
50
|
+
nextTasksCount: project.nextTasksCount ?? 0,
|
|
51
|
+
completionRate: project.completionRate ?? 0
|
|
42
52
|
}
|
|
43
53
|
}
|
|
44
54
|
} catch {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/// <reference types="next" />
|
|
2
2
|
/// <reference types="next/image-types/global" />
|
|
3
|
-
import "./.next/
|
|
3
|
+
import "./.next/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.
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"scripts": {
|
|
6
6
|
"dev": "NODE_ENV=development PORT=9471 bun server.ts",
|
|
7
7
|
"build": "bun next build",
|
|
8
|
-
"start": "NODE_ENV=production PORT=9472
|
|
8
|
+
"start": "NODE_ENV=production PORT=9472 node --import tsx server.ts",
|
|
9
9
|
"lint": "eslint"
|
|
10
10
|
},
|
|
11
11
|
"dependencies": {
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
"@radix-ui/react-dialog": "^1.1.15",
|
|
14
14
|
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
|
15
15
|
"@radix-ui/react-scroll-area": "^1.2.10",
|
|
16
|
+
"@radix-ui/react-select": "^2.2.6",
|
|
16
17
|
"@radix-ui/react-slot": "^1.2.4",
|
|
17
18
|
"@radix-ui/react-tabs": "^1.1.13",
|
|
18
19
|
"@radix-ui/react-tooltip": "^1.2.8",
|
|
@@ -31,15 +32,18 @@
|
|
|
31
32
|
"react": "19.2.0",
|
|
32
33
|
"react-dom": "19.2.0",
|
|
33
34
|
"react-markdown": "^10.1.0",
|
|
35
|
+
"react-resizable-panels": "^3.0.6",
|
|
34
36
|
"recharts": "^3.5.1",
|
|
35
37
|
"remark-gfm": "^4.0.1",
|
|
36
38
|
"tailwind-merge": "^3.4.0",
|
|
39
|
+
"vaul": "^1.1.2",
|
|
37
40
|
"ws": "^8.18.3",
|
|
38
41
|
"zod": "^4.1.13"
|
|
39
42
|
},
|
|
40
43
|
"devDependencies": {
|
|
41
44
|
"@tailwindcss/postcss": "^4",
|
|
42
45
|
"@types/bun": "latest",
|
|
46
|
+
"tsx": "^4.21.0",
|
|
43
47
|
"@types/node": "^20",
|
|
44
48
|
"@types/react": "^19",
|
|
45
49
|
"@types/react-dom": "^19",
|