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.
Files changed (193) hide show
  1. package/CHANGELOG.md +106 -0
  2. package/bin/prjct +10 -13
  3. package/core/agentic/memory-system/semantic-memories.ts +2 -1
  4. package/core/agentic/plan-mode/plan-mode.ts +2 -1
  5. package/core/agentic/prompt-builder.ts +22 -43
  6. package/core/agentic/services.ts +5 -5
  7. package/core/agentic/smart-context.ts +7 -2
  8. package/core/command-registry/core-commands.ts +54 -29
  9. package/core/command-registry/optional-commands.ts +64 -0
  10. package/core/command-registry/setup-commands.ts +18 -3
  11. package/core/commands/analysis.ts +21 -68
  12. package/core/commands/analytics.ts +247 -213
  13. package/core/commands/base.ts +1 -1
  14. package/core/commands/index.ts +41 -36
  15. package/core/commands/maintenance.ts +300 -31
  16. package/core/commands/planning.ts +233 -22
  17. package/core/commands/setup.ts +3 -8
  18. package/core/commands/shipping.ts +14 -18
  19. package/core/commands/types.ts +8 -6
  20. package/core/commands/workflow.ts +105 -100
  21. package/core/context/generator.ts +317 -0
  22. package/core/context-sync.ts +7 -350
  23. package/core/data/index.ts +13 -32
  24. package/core/data/md-ideas-manager.ts +155 -0
  25. package/core/data/md-queue-manager.ts +4 -3
  26. package/core/data/md-shipped-manager.ts +90 -0
  27. package/core/data/md-state-manager.ts +11 -7
  28. package/core/domain/agent-generator.ts +23 -63
  29. package/core/events/index.ts +143 -0
  30. package/core/index.ts +17 -14
  31. package/core/infrastructure/capability-installer.ts +13 -149
  32. package/core/infrastructure/migrator/project-scanner.ts +2 -1
  33. package/core/infrastructure/path-manager.ts +4 -6
  34. package/core/infrastructure/setup.ts +3 -0
  35. package/core/infrastructure/uuid-migration.ts +750 -0
  36. package/core/outcomes/recorder.ts +2 -1
  37. package/core/plugin/loader.ts +4 -7
  38. package/core/plugin/registry.ts +3 -3
  39. package/core/schemas/index.ts +23 -25
  40. package/core/schemas/state.ts +1 -0
  41. package/core/serializers/ideas-serializer.ts +187 -0
  42. package/core/serializers/index.ts +16 -0
  43. package/core/serializers/shipped-serializer.ts +108 -0
  44. package/core/session/utils.ts +3 -9
  45. package/core/storage/ideas-storage.ts +273 -0
  46. package/core/storage/index.ts +204 -0
  47. package/core/storage/queue-storage.ts +297 -0
  48. package/core/storage/shipped-storage.ts +223 -0
  49. package/core/storage/state-storage.ts +235 -0
  50. package/core/storage/storage-manager.ts +175 -0
  51. package/package.json +1 -1
  52. package/packages/web/app/api/projects/[id]/momentum/route.ts +257 -0
  53. package/packages/web/app/api/sessions/current/route.ts +132 -0
  54. package/packages/web/app/api/sessions/history/route.ts +96 -14
  55. package/packages/web/app/globals.css +5 -0
  56. package/packages/web/app/layout.tsx +2 -0
  57. package/packages/web/app/project/[id]/code/layout.tsx +18 -0
  58. package/packages/web/app/project/[id]/code/page.tsx +408 -0
  59. package/packages/web/app/project/[id]/page.tsx +359 -389
  60. package/packages/web/app/project/[id]/reports/page.tsx +59 -0
  61. package/packages/web/app/project/[id]/reports/print/page.tsx +58 -0
  62. package/packages/web/components/ActivityTimeline/ActivityTimeline.tsx +0 -1
  63. package/packages/web/components/AgentsCard/AgentsCard.tsx +64 -34
  64. package/packages/web/components/AgentsCard/AgentsCard.types.ts +1 -0
  65. package/packages/web/components/AppSidebar/AppSidebar.tsx +135 -11
  66. package/packages/web/components/BentoCard/BentoCard.constants.ts +3 -3
  67. package/packages/web/components/BentoCard/BentoCard.tsx +2 -1
  68. package/packages/web/components/BentoGrid/BentoGrid.tsx +2 -2
  69. package/packages/web/components/BlockersCard/BlockersCard.tsx +65 -57
  70. package/packages/web/components/BlockersCard/BlockersCard.types.ts +1 -0
  71. package/packages/web/components/CommandBar/CommandBar.tsx +67 -0
  72. package/packages/web/components/CommandBar/index.ts +1 -0
  73. package/packages/web/components/DashboardContent/DashboardContent.tsx +35 -5
  74. package/packages/web/components/DateGroup/DateGroup.tsx +1 -1
  75. package/packages/web/components/EmptyState/EmptyState.tsx +39 -21
  76. package/packages/web/components/EmptyState/EmptyState.types.ts +1 -0
  77. package/packages/web/components/EventRow/EventRow.tsx +4 -4
  78. package/packages/web/components/EventRow/EventRow.utils.ts +3 -3
  79. package/packages/web/components/HeroSection/HeroSection.tsx +52 -15
  80. package/packages/web/components/HeroSection/HeroSection.types.ts +4 -4
  81. package/packages/web/components/HeroSection/HeroSection.utils.ts +7 -3
  82. package/packages/web/components/IdeasCard/IdeasCard.tsx +94 -27
  83. package/packages/web/components/IdeasCard/IdeasCard.types.ts +1 -0
  84. package/packages/web/components/MasonryGrid/MasonryGrid.tsx +18 -0
  85. package/packages/web/components/MasonryGrid/index.ts +1 -0
  86. package/packages/web/components/MomentumWidget/MomentumWidget.tsx +119 -0
  87. package/packages/web/components/MomentumWidget/MomentumWidget.types.ts +16 -0
  88. package/packages/web/components/MomentumWidget/index.ts +2 -0
  89. package/packages/web/components/NowCard/NowCard.tsx +81 -56
  90. package/packages/web/components/NowCard/NowCard.types.ts +1 -0
  91. package/packages/web/components/PageHeader/PageHeader.tsx +24 -0
  92. package/packages/web/components/PageHeader/index.ts +1 -0
  93. package/packages/web/components/ProgressRing/ProgressRing.constants.ts +2 -2
  94. package/packages/web/components/ProjectAvatar/ProjectAvatar.tsx +2 -2
  95. package/packages/web/components/ProjectColorDot/ProjectColorDot.tsx +37 -0
  96. package/packages/web/components/ProjectColorDot/index.ts +1 -0
  97. package/packages/web/components/ProjectSelectorModal/ProjectSelectorModal.tsx +104 -0
  98. package/packages/web/components/ProjectSelectorModal/index.ts +1 -0
  99. package/packages/web/components/Providers/Providers.tsx +4 -1
  100. package/packages/web/components/QueueCard/QueueCard.tsx +78 -25
  101. package/packages/web/components/QueueCard/QueueCard.types.ts +1 -0
  102. package/packages/web/components/QueueCard/QueueCard.utils.ts +3 -3
  103. package/packages/web/components/RecoverCard/RecoverCard.tsx +72 -0
  104. package/packages/web/components/RecoverCard/RecoverCard.types.ts +16 -0
  105. package/packages/web/components/RecoverCard/index.ts +2 -0
  106. package/packages/web/components/RoadmapCard/RoadmapCard.tsx +101 -33
  107. package/packages/web/components/RoadmapCard/RoadmapCard.types.ts +1 -0
  108. package/packages/web/components/ShipsCard/ShipsCard.tsx +71 -28
  109. package/packages/web/components/ShipsCard/ShipsCard.types.ts +2 -0
  110. package/packages/web/components/SparklineChart/SparklineChart.tsx +20 -18
  111. package/packages/web/components/StatsMasonry/StatsMasonry.tsx +95 -0
  112. package/packages/web/components/StatsMasonry/index.ts +1 -0
  113. package/packages/web/components/StreakCard/StreakCard.tsx +37 -35
  114. package/packages/web/components/TasksCounter/TasksCounter.tsx +1 -1
  115. package/packages/web/components/TechStackBadges/TechStackBadges.tsx +12 -4
  116. package/packages/web/components/TerminalDock/DockToggleTab.tsx +29 -0
  117. package/packages/web/components/TerminalDock/TerminalDock.tsx +386 -0
  118. package/packages/web/components/TerminalDock/TerminalDockTab.tsx +130 -0
  119. package/packages/web/components/TerminalDock/TerminalTabBar.tsx +142 -0
  120. package/packages/web/components/TerminalDock/index.ts +2 -0
  121. package/packages/web/components/VelocityBadge/VelocityBadge.tsx +8 -3
  122. package/packages/web/components/VelocityCard/VelocityCard.tsx +49 -47
  123. package/packages/web/components/WeeklyReports/PrintableReport.tsx +259 -0
  124. package/packages/web/components/WeeklyReports/ReportPreviewCard.tsx +187 -0
  125. package/packages/web/components/WeeklyReports/WeekCalendar.tsx +288 -0
  126. package/packages/web/components/WeeklyReports/WeeklyReports.tsx +149 -0
  127. package/packages/web/components/WeeklyReports/index.ts +4 -0
  128. package/packages/web/components/WeeklySparkline/WeeklySparkline.tsx +16 -4
  129. package/packages/web/components/WeeklySparkline/WeeklySparkline.types.ts +1 -0
  130. package/packages/web/components/charts/SessionsChart.tsx +6 -3
  131. package/packages/web/components/ui/dialog.tsx +143 -0
  132. package/packages/web/components/ui/drawer.tsx +135 -0
  133. package/packages/web/components/ui/select.tsx +187 -0
  134. package/packages/web/context/GlobalTerminalContext.tsx +538 -0
  135. package/packages/web/lib/commands.ts +81 -0
  136. package/packages/web/lib/generate-week-report.ts +285 -0
  137. package/packages/web/lib/parse-prjct-files.ts +56 -55
  138. package/packages/web/lib/project-colors.ts +58 -0
  139. package/packages/web/lib/projects.ts +58 -5
  140. package/packages/web/lib/services/projects.server.ts +11 -1
  141. package/packages/web/next-env.d.ts +1 -1
  142. package/packages/web/package.json +5 -1
  143. package/templates/commands/analyze.md +39 -3
  144. package/templates/commands/ask.md +58 -3
  145. package/templates/commands/bug.md +117 -26
  146. package/templates/commands/dash.md +95 -158
  147. package/templates/commands/done.md +130 -148
  148. package/templates/commands/feature.md +125 -103
  149. package/templates/commands/git.md +18 -3
  150. package/templates/commands/idea.md +121 -38
  151. package/templates/commands/init.md +124 -20
  152. package/templates/commands/migrate-all.md +63 -28
  153. package/templates/commands/migrate.md +140 -0
  154. package/templates/commands/next.md +115 -5
  155. package/templates/commands/now.md +146 -82
  156. package/templates/commands/pause.md +89 -74
  157. package/templates/commands/redo.md +6 -4
  158. package/templates/commands/resume.md +141 -59
  159. package/templates/commands/ship.md +103 -231
  160. package/templates/commands/spec.md +98 -8
  161. package/templates/commands/suggest.md +22 -2
  162. package/templates/commands/sync.md +192 -203
  163. package/templates/commands/undo.md +6 -4
  164. package/core/data/agents-manager.ts +0 -76
  165. package/core/data/analysis-manager.ts +0 -83
  166. package/core/data/base-manager.ts +0 -156
  167. package/core/data/ideas-manager.ts +0 -81
  168. package/core/data/outcomes-manager.ts +0 -96
  169. package/core/data/project-manager.ts +0 -75
  170. package/core/data/roadmap-manager.ts +0 -118
  171. package/core/data/shipped-manager.ts +0 -65
  172. package/core/data/state-manager.ts +0 -214
  173. package/core/state/index.ts +0 -25
  174. package/core/state/manager.ts +0 -376
  175. package/core/state/types.ts +0 -185
  176. package/core/utils/project-capabilities.ts +0 -156
  177. package/core/view-generator.ts +0 -536
  178. package/packages/web/app/project/[id]/stats/loading.tsx +0 -43
  179. package/packages/web/app/project/[id]/stats/page.tsx +0 -253
  180. package/templates/agent-assignment.md +0 -72
  181. package/templates/analysis/project-analysis.md +0 -78
  182. package/templates/checklists/accessibility.md +0 -33
  183. package/templates/commands/build.md +0 -17
  184. package/templates/commands/decision.md +0 -226
  185. package/templates/commands/fix.md +0 -79
  186. package/templates/commands/help.md +0 -61
  187. package/templates/commands/progress.md +0 -14
  188. package/templates/commands/recap.md +0 -14
  189. package/templates/commands/roadmap.md +0 -52
  190. package/templates/commands/status.md +0 -17
  191. package/templates/commands/task.md +0 -63
  192. package/templates/commands/work.md +0 -44
  193. 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
@@ -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'
@@ -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