prjct-cli 0.13.3 → 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
|
@@ -1,214 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* State Manager
|
|
3
|
-
*
|
|
4
|
-
* Manages state.json - the unified project state.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { BaseManager } from './base-manager'
|
|
8
|
-
import type {
|
|
9
|
-
StateSchema,
|
|
10
|
-
CurrentTask,
|
|
11
|
-
QueuedTask,
|
|
12
|
-
RecentActivity,
|
|
13
|
-
Stats
|
|
14
|
-
} from '../schemas'
|
|
15
|
-
import { DEFAULT_STATE, DEFAULT_STATS } from '../schemas'
|
|
16
|
-
|
|
17
|
-
const MAX_RECENT_ACTIVITY = 10
|
|
18
|
-
|
|
19
|
-
class StateManager extends BaseManager<StateSchema> {
|
|
20
|
-
constructor() {
|
|
21
|
-
super('state.json')
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
protected getDefault(projectId: string): StateSchema {
|
|
25
|
-
return {
|
|
26
|
-
...DEFAULT_STATE,
|
|
27
|
-
projectId,
|
|
28
|
-
lastSync: new Date().toISOString()
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
// =========== Current Task ===========
|
|
33
|
-
|
|
34
|
-
async getCurrentTask(projectId: string): Promise<CurrentTask | null> {
|
|
35
|
-
const state = await this.read(projectId)
|
|
36
|
-
return state.currentTask
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
async setCurrentTask(projectId: string, task: CurrentTask | null): Promise<StateSchema> {
|
|
40
|
-
return this.update(projectId, (state) => ({
|
|
41
|
-
...state,
|
|
42
|
-
currentTask: task,
|
|
43
|
-
lastSync: new Date().toISOString()
|
|
44
|
-
}))
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
async startTask(
|
|
48
|
-
projectId: string,
|
|
49
|
-
task: Omit<CurrentTask, 'startedAt'>
|
|
50
|
-
): Promise<StateSchema> {
|
|
51
|
-
const currentTask: CurrentTask = {
|
|
52
|
-
...task,
|
|
53
|
-
startedAt: new Date().toISOString()
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
return this.update(projectId, (state) => ({
|
|
57
|
-
...state,
|
|
58
|
-
currentTask,
|
|
59
|
-
recentActivity: [
|
|
60
|
-
{
|
|
61
|
-
type: 'session_started' as const,
|
|
62
|
-
description: task.description,
|
|
63
|
-
timestamp: new Date().toISOString()
|
|
64
|
-
},
|
|
65
|
-
...state.recentActivity
|
|
66
|
-
].slice(0, MAX_RECENT_ACTIVITY),
|
|
67
|
-
lastSync: new Date().toISOString()
|
|
68
|
-
}))
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
async completeTask(projectId: string, duration: string): Promise<StateSchema> {
|
|
72
|
-
const state = await this.read(projectId)
|
|
73
|
-
if (!state.currentTask) {
|
|
74
|
-
throw new Error('No active task to complete')
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
const activity: RecentActivity = {
|
|
78
|
-
type: 'task_completed',
|
|
79
|
-
description: state.currentTask.description,
|
|
80
|
-
timestamp: new Date().toISOString(),
|
|
81
|
-
duration
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
return this.update(projectId, (s) => ({
|
|
85
|
-
...s,
|
|
86
|
-
currentTask: null,
|
|
87
|
-
recentActivity: [activity, ...s.recentActivity].slice(0, MAX_RECENT_ACTIVITY),
|
|
88
|
-
stats: {
|
|
89
|
-
...s.stats,
|
|
90
|
-
tasksToday: s.stats.tasksToday + 1
|
|
91
|
-
},
|
|
92
|
-
lastSync: new Date().toISOString()
|
|
93
|
-
}))
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
async pauseTask(projectId: string, reason?: string): Promise<StateSchema> {
|
|
97
|
-
const state = await this.read(projectId)
|
|
98
|
-
if (!state.currentTask) {
|
|
99
|
-
throw new Error('No active task to pause')
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
return this.update(projectId, (s) => ({
|
|
103
|
-
...s,
|
|
104
|
-
currentTask: s.currentTask
|
|
105
|
-
? {
|
|
106
|
-
...s.currentTask,
|
|
107
|
-
pausedAt: new Date().toISOString(),
|
|
108
|
-
pauseReason: reason
|
|
109
|
-
}
|
|
110
|
-
: null,
|
|
111
|
-
lastSync: new Date().toISOString()
|
|
112
|
-
}))
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
async resumeTask(projectId: string): Promise<StateSchema> {
|
|
116
|
-
const state = await this.read(projectId)
|
|
117
|
-
if (!state.currentTask?.pausedAt) {
|
|
118
|
-
throw new Error('No paused task to resume')
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
return this.update(projectId, (s) => ({
|
|
122
|
-
...s,
|
|
123
|
-
currentTask: s.currentTask
|
|
124
|
-
? {
|
|
125
|
-
...s.currentTask,
|
|
126
|
-
pausedAt: undefined,
|
|
127
|
-
pauseReason: undefined
|
|
128
|
-
}
|
|
129
|
-
: null,
|
|
130
|
-
lastSync: new Date().toISOString()
|
|
131
|
-
}))
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// =========== Queue ===========
|
|
135
|
-
|
|
136
|
-
async getQueue(projectId: string): Promise<QueuedTask[]> {
|
|
137
|
-
const state = await this.read(projectId)
|
|
138
|
-
return state.queue
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
async addToQueue(
|
|
142
|
-
projectId: string,
|
|
143
|
-
task: Omit<QueuedTask, 'id' | 'createdAt'>
|
|
144
|
-
): Promise<StateSchema> {
|
|
145
|
-
const queuedTask: QueuedTask = {
|
|
146
|
-
...task,
|
|
147
|
-
id: `task_${Date.now()}`,
|
|
148
|
-
createdAt: new Date().toISOString()
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
return this.update(projectId, (state) => ({
|
|
152
|
-
...state,
|
|
153
|
-
queue: [...state.queue, queuedTask].sort((a, b) => {
|
|
154
|
-
const priorityOrder = { critical: 0, high: 1, medium: 2, low: 3 }
|
|
155
|
-
return priorityOrder[a.priority] - priorityOrder[b.priority]
|
|
156
|
-
}),
|
|
157
|
-
lastSync: new Date().toISOString()
|
|
158
|
-
}))
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
async removeFromQueue(projectId: string, taskId: string): Promise<StateSchema> {
|
|
162
|
-
return this.update(projectId, (state) => ({
|
|
163
|
-
...state,
|
|
164
|
-
queue: state.queue.filter((t) => t.id !== taskId),
|
|
165
|
-
lastSync: new Date().toISOString()
|
|
166
|
-
}))
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
async getNextTask(projectId: string): Promise<QueuedTask | null> {
|
|
170
|
-
const state = await this.read(projectId)
|
|
171
|
-
return state.queue.find((t) => !t.blockedReason) || null
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
// =========== Stats ===========
|
|
175
|
-
|
|
176
|
-
async getStats(projectId: string): Promise<Stats> {
|
|
177
|
-
const state = await this.read(projectId)
|
|
178
|
-
return state.stats
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
async updateStats(projectId: string, stats: Partial<Stats>): Promise<StateSchema> {
|
|
182
|
-
return this.update(projectId, (state) => ({
|
|
183
|
-
...state,
|
|
184
|
-
stats: { ...state.stats, ...stats },
|
|
185
|
-
lastSync: new Date().toISOString()
|
|
186
|
-
}))
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
async resetDailyStats(projectId: string): Promise<StateSchema> {
|
|
190
|
-
return this.update(projectId, (state) => ({
|
|
191
|
-
...state,
|
|
192
|
-
stats: { ...state.stats, tasksToday: 0 },
|
|
193
|
-
lastSync: new Date().toISOString()
|
|
194
|
-
}))
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
// =========== Activity ===========
|
|
198
|
-
|
|
199
|
-
async addActivity(projectId: string, activity: RecentActivity): Promise<StateSchema> {
|
|
200
|
-
return this.update(projectId, (state) => ({
|
|
201
|
-
...state,
|
|
202
|
-
recentActivity: [activity, ...state.recentActivity].slice(0, MAX_RECENT_ACTIVITY),
|
|
203
|
-
lastSync: new Date().toISOString()
|
|
204
|
-
}))
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
async getRecentActivity(projectId: string): Promise<RecentActivity[]> {
|
|
208
|
-
const state = await this.read(projectId)
|
|
209
|
-
return state.recentActivity
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
export const stateManager = new StateManager()
|
|
214
|
-
export default stateManager
|
package/core/state/index.ts
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* State Module
|
|
3
|
-
*
|
|
4
|
-
* Unified project state management.
|
|
5
|
-
* Single source of truth replacing scattered file reads.
|
|
6
|
-
*
|
|
7
|
-
* @example
|
|
8
|
-
* ```typescript
|
|
9
|
-
* import stateManager from './state'
|
|
10
|
-
*
|
|
11
|
-
* // Read state
|
|
12
|
-
* const state = await stateManager.read(projectId)
|
|
13
|
-
*
|
|
14
|
-
* // Update state
|
|
15
|
-
* await stateManager.startTask(projectId, { id: 'task_1', description: 'auth' })
|
|
16
|
-
* await stateManager.completeTask(projectId, '2h 15m')
|
|
17
|
-
* ```
|
|
18
|
-
*/
|
|
19
|
-
|
|
20
|
-
import stateManager from './manager'
|
|
21
|
-
|
|
22
|
-
export { StateManager } from './manager'
|
|
23
|
-
export { stateManager }
|
|
24
|
-
export default stateManager
|
|
25
|
-
export * from './types'
|
package/core/state/manager.ts
DELETED
|
@@ -1,376 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* State Manager
|
|
3
|
-
*
|
|
4
|
-
* Manages unified project state with atomic read/write operations.
|
|
5
|
-
* Replaces scattered file reads with single state.json access.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import path from 'path'
|
|
9
|
-
import * as fileHelper from '../utils/file-helper'
|
|
10
|
-
import pathManager from '../infrastructure/path-manager'
|
|
11
|
-
import type {
|
|
12
|
-
ProjectState,
|
|
13
|
-
CurrentTask,
|
|
14
|
-
QueuedTask,
|
|
15
|
-
ActiveFeature,
|
|
16
|
-
PerformanceStats,
|
|
17
|
-
RecentActivity,
|
|
18
|
-
StateUpdate,
|
|
19
|
-
} from './types'
|
|
20
|
-
import { DEFAULT_STATE } from './types'
|
|
21
|
-
|
|
22
|
-
const STATE_FILENAME = 'state.json'
|
|
23
|
-
const MAX_RECENT_ACTIVITY = 10
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* StateManager - Single source of truth for project state.
|
|
27
|
-
*/
|
|
28
|
-
export class StateManager {
|
|
29
|
-
private cache: Map<string, ProjectState> = new Map()
|
|
30
|
-
private cacheTimeout = 5000 // 5 seconds
|
|
31
|
-
private lastRead: Map<string, number> = new Map()
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Get state file path for a project.
|
|
35
|
-
*/
|
|
36
|
-
private getStatePath(projectId: string): string {
|
|
37
|
-
const globalPath = pathManager.getGlobalProjectPath(projectId)
|
|
38
|
-
return path.join(globalPath, 'core', STATE_FILENAME)
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Read project state.
|
|
43
|
-
* Uses cache if available and fresh.
|
|
44
|
-
*/
|
|
45
|
-
async read(projectId: string): Promise<ProjectState> {
|
|
46
|
-
const now = Date.now()
|
|
47
|
-
const lastReadTime = this.lastRead.get(projectId) || 0
|
|
48
|
-
|
|
49
|
-
// Return cached if fresh
|
|
50
|
-
if (now - lastReadTime < this.cacheTimeout && this.cache.has(projectId)) {
|
|
51
|
-
return this.cache.get(projectId)!
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
const statePath = this.getStatePath(projectId)
|
|
55
|
-
const state = await fileHelper.readJson<ProjectState>(statePath, {
|
|
56
|
-
...DEFAULT_STATE,
|
|
57
|
-
projectId,
|
|
58
|
-
})
|
|
59
|
-
|
|
60
|
-
// Update cache
|
|
61
|
-
this.cache.set(projectId, state)
|
|
62
|
-
this.lastRead.set(projectId, now)
|
|
63
|
-
|
|
64
|
-
return state
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* Write project state atomically.
|
|
69
|
-
*/
|
|
70
|
-
async write(projectId: string, state: ProjectState): Promise<void> {
|
|
71
|
-
const statePath = this.getStatePath(projectId)
|
|
72
|
-
|
|
73
|
-
// Ensure directory exists
|
|
74
|
-
await fileHelper.ensureDir(path.dirname(statePath))
|
|
75
|
-
|
|
76
|
-
// Update lastSync
|
|
77
|
-
const updatedState: ProjectState = {
|
|
78
|
-
...state,
|
|
79
|
-
lastSync: new Date().toISOString(),
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
await fileHelper.writeJson(statePath, updatedState)
|
|
83
|
-
|
|
84
|
-
// Update cache
|
|
85
|
-
this.cache.set(projectId, updatedState)
|
|
86
|
-
this.lastRead.set(projectId, Date.now())
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Apply an update to project state.
|
|
91
|
-
*/
|
|
92
|
-
async update(projectId: string, update: StateUpdate): Promise<ProjectState> {
|
|
93
|
-
const state = await this.read(projectId)
|
|
94
|
-
const newState = this.applyUpdate(state, update)
|
|
95
|
-
await this.write(projectId, newState)
|
|
96
|
-
return newState
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* Apply multiple updates atomically.
|
|
101
|
-
*/
|
|
102
|
-
async batchUpdate(projectId: string, updates: StateUpdate[]): Promise<ProjectState> {
|
|
103
|
-
let state = await this.read(projectId)
|
|
104
|
-
for (const update of updates) {
|
|
105
|
-
state = this.applyUpdate(state, update)
|
|
106
|
-
}
|
|
107
|
-
await this.write(projectId, state)
|
|
108
|
-
return state
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/**
|
|
112
|
-
* Apply a single update to state.
|
|
113
|
-
*/
|
|
114
|
-
private applyUpdate(state: ProjectState, update: StateUpdate): ProjectState {
|
|
115
|
-
switch (update.type) {
|
|
116
|
-
case 'SET_CURRENT_TASK':
|
|
117
|
-
return { ...state, currentTask: update.task }
|
|
118
|
-
|
|
119
|
-
case 'ADD_TO_QUEUE':
|
|
120
|
-
return {
|
|
121
|
-
...state,
|
|
122
|
-
queue: [...state.queue, update.task].sort((a, b) => {
|
|
123
|
-
const priorityOrder = { critical: 0, high: 1, medium: 2, low: 3 }
|
|
124
|
-
return priorityOrder[a.priority] - priorityOrder[b.priority]
|
|
125
|
-
}),
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
case 'REMOVE_FROM_QUEUE':
|
|
129
|
-
return {
|
|
130
|
-
...state,
|
|
131
|
-
queue: state.queue.filter((t) => t.id !== update.taskId),
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
case 'UPDATE_QUEUE_TASK':
|
|
135
|
-
return {
|
|
136
|
-
...state,
|
|
137
|
-
queue: state.queue.map((t) =>
|
|
138
|
-
t.id === update.taskId ? { ...t, ...update.updates } : t
|
|
139
|
-
),
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
case 'SET_ACTIVE_FEATURE':
|
|
143
|
-
return { ...state, activeFeature: update.feature }
|
|
144
|
-
|
|
145
|
-
case 'UPDATE_STATS':
|
|
146
|
-
return {
|
|
147
|
-
...state,
|
|
148
|
-
stats: { ...state.stats, ...update.stats },
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
case 'ADD_ACTIVITY':
|
|
152
|
-
return {
|
|
153
|
-
...state,
|
|
154
|
-
recentActivity: [update.activity, ...state.recentActivity].slice(
|
|
155
|
-
0,
|
|
156
|
-
MAX_RECENT_ACTIVITY
|
|
157
|
-
),
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
case 'SYNC':
|
|
161
|
-
return { ...state, lastSync: new Date().toISOString() }
|
|
162
|
-
|
|
163
|
-
default:
|
|
164
|
-
return state
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
// =========== Convenience Methods ===========
|
|
169
|
-
|
|
170
|
-
/**
|
|
171
|
-
* Start a new task.
|
|
172
|
-
*/
|
|
173
|
-
async startTask(
|
|
174
|
-
projectId: string,
|
|
175
|
-
task: Omit<CurrentTask, 'startedAt'>
|
|
176
|
-
): Promise<ProjectState> {
|
|
177
|
-
const currentTask: CurrentTask = {
|
|
178
|
-
...task,
|
|
179
|
-
startedAt: new Date().toISOString(),
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
return this.batchUpdate(projectId, [
|
|
183
|
-
{ type: 'SET_CURRENT_TASK', task: currentTask },
|
|
184
|
-
{
|
|
185
|
-
type: 'ADD_ACTIVITY',
|
|
186
|
-
activity: {
|
|
187
|
-
type: 'session_started',
|
|
188
|
-
description: task.description,
|
|
189
|
-
timestamp: new Date().toISOString(),
|
|
190
|
-
},
|
|
191
|
-
},
|
|
192
|
-
])
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
/**
|
|
196
|
-
* Complete the current task.
|
|
197
|
-
*/
|
|
198
|
-
async completeTask(projectId: string, duration: string): Promise<ProjectState> {
|
|
199
|
-
const state = await this.read(projectId)
|
|
200
|
-
if (!state.currentTask) {
|
|
201
|
-
throw new Error('No active task to complete')
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
const activity: RecentActivity = {
|
|
205
|
-
type: 'task_completed',
|
|
206
|
-
description: state.currentTask.description,
|
|
207
|
-
timestamp: new Date().toISOString(),
|
|
208
|
-
duration,
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
// Update feature progress if linked
|
|
212
|
-
let featureUpdate: StateUpdate | null = null
|
|
213
|
-
if (state.activeFeature && state.currentTask.featureId === state.activeFeature.id) {
|
|
214
|
-
featureUpdate = {
|
|
215
|
-
type: 'SET_ACTIVE_FEATURE',
|
|
216
|
-
feature: {
|
|
217
|
-
...state.activeFeature,
|
|
218
|
-
tasksCompleted: state.activeFeature.tasksCompleted + 1,
|
|
219
|
-
tasksRemaining: Math.max(0, state.activeFeature.tasksRemaining - 1),
|
|
220
|
-
},
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
const updates: StateUpdate[] = [
|
|
225
|
-
{ type: 'SET_CURRENT_TASK', task: null },
|
|
226
|
-
{ type: 'ADD_ACTIVITY', activity },
|
|
227
|
-
{
|
|
228
|
-
type: 'UPDATE_STATS',
|
|
229
|
-
stats: { tasksToday: state.stats.tasksToday + 1 },
|
|
230
|
-
},
|
|
231
|
-
]
|
|
232
|
-
|
|
233
|
-
if (featureUpdate) {
|
|
234
|
-
updates.push(featureUpdate)
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
return this.batchUpdate(projectId, updates)
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
/**
|
|
241
|
-
* Pause the current task.
|
|
242
|
-
*/
|
|
243
|
-
async pauseTask(projectId: string, reason?: string): Promise<ProjectState> {
|
|
244
|
-
const state = await this.read(projectId)
|
|
245
|
-
if (!state.currentTask) {
|
|
246
|
-
throw new Error('No active task to pause')
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
const pausedTask: CurrentTask = {
|
|
250
|
-
...state.currentTask,
|
|
251
|
-
pausedAt: new Date().toISOString(),
|
|
252
|
-
pauseReason: reason,
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
return this.update(projectId, { type: 'SET_CURRENT_TASK', task: pausedTask })
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
/**
|
|
259
|
-
* Resume a paused task.
|
|
260
|
-
*/
|
|
261
|
-
async resumeTask(projectId: string): Promise<ProjectState> {
|
|
262
|
-
const state = await this.read(projectId)
|
|
263
|
-
if (!state.currentTask || !state.currentTask.pausedAt) {
|
|
264
|
-
throw new Error('No paused task to resume')
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
const resumedTask: CurrentTask = {
|
|
268
|
-
...state.currentTask,
|
|
269
|
-
pausedAt: undefined,
|
|
270
|
-
pauseReason: undefined,
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
return this.update(projectId, { type: 'SET_CURRENT_TASK', task: resumedTask })
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
/**
|
|
277
|
-
* Add a task to the queue.
|
|
278
|
-
*/
|
|
279
|
-
async addToQueue(
|
|
280
|
-
projectId: string,
|
|
281
|
-
task: Omit<QueuedTask, 'id' | 'createdAt'>
|
|
282
|
-
): Promise<ProjectState> {
|
|
283
|
-
const queuedTask: QueuedTask = {
|
|
284
|
-
...task,
|
|
285
|
-
id: `task_${Date.now()}`,
|
|
286
|
-
createdAt: new Date().toISOString(),
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
return this.update(projectId, { type: 'ADD_TO_QUEUE', task: queuedTask })
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
/**
|
|
293
|
-
* Get the next task from queue.
|
|
294
|
-
*/
|
|
295
|
-
async getNextTask(projectId: string): Promise<QueuedTask | null> {
|
|
296
|
-
const state = await this.read(projectId)
|
|
297
|
-
return state.queue.find((t) => t.blockedReason === undefined) || null
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
/**
|
|
301
|
-
* Start a feature.
|
|
302
|
-
*/
|
|
303
|
-
async startFeature(
|
|
304
|
-
projectId: string,
|
|
305
|
-
feature: Omit<ActiveFeature, 'startedAt' | 'tasksCompleted' | 'status'>
|
|
306
|
-
): Promise<ProjectState> {
|
|
307
|
-
const activeFeature: ActiveFeature = {
|
|
308
|
-
...feature,
|
|
309
|
-
status: 'in_progress',
|
|
310
|
-
tasksCompleted: 0,
|
|
311
|
-
startedAt: new Date().toISOString(),
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
return this.update(projectId, { type: 'SET_ACTIVE_FEATURE', feature: activeFeature })
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
/**
|
|
318
|
-
* Ship a feature.
|
|
319
|
-
*/
|
|
320
|
-
async shipFeature(projectId: string): Promise<ProjectState> {
|
|
321
|
-
const state = await this.read(projectId)
|
|
322
|
-
if (!state.activeFeature) {
|
|
323
|
-
throw new Error('No active feature to ship')
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
const activity: RecentActivity = {
|
|
327
|
-
type: 'feature_shipped',
|
|
328
|
-
description: state.activeFeature.name,
|
|
329
|
-
timestamp: new Date().toISOString(),
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
return this.batchUpdate(projectId, [
|
|
333
|
-
{ type: 'SET_ACTIVE_FEATURE', feature: null },
|
|
334
|
-
{ type: 'ADD_ACTIVITY', activity },
|
|
335
|
-
])
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
/**
|
|
339
|
-
* Clear cache for a project.
|
|
340
|
-
*/
|
|
341
|
-
clearCache(projectId?: string): void {
|
|
342
|
-
if (projectId) {
|
|
343
|
-
this.cache.delete(projectId)
|
|
344
|
-
this.lastRead.delete(projectId)
|
|
345
|
-
} else {
|
|
346
|
-
this.cache.clear()
|
|
347
|
-
this.lastRead.clear()
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
/**
|
|
352
|
-
* Check if project has state file.
|
|
353
|
-
*/
|
|
354
|
-
async exists(projectId: string): Promise<boolean> {
|
|
355
|
-
const statePath = this.getStatePath(projectId)
|
|
356
|
-
return fileHelper.fileExists(statePath)
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
/**
|
|
360
|
-
* Initialize state for a new project.
|
|
361
|
-
*/
|
|
362
|
-
async initialize(projectId: string): Promise<ProjectState> {
|
|
363
|
-
const initialState: ProjectState = {
|
|
364
|
-
...DEFAULT_STATE,
|
|
365
|
-
projectId,
|
|
366
|
-
lastSync: new Date().toISOString(),
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
await this.write(projectId, initialState)
|
|
370
|
-
return initialState
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
// Singleton instance
|
|
375
|
-
const stateManager = new StateManager()
|
|
376
|
-
export default stateManager
|