prjct-cli 0.12.1 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +43 -0
- package/CLAUDE.md +18 -6
- package/bin/serve.js +12 -30
- package/core/data/index.ts +19 -5
- package/core/data/md-base-manager.ts +203 -0
- package/core/data/md-queue-manager.ts +179 -0
- package/core/data/md-state-manager.ts +133 -0
- package/core/serializers/index.ts +20 -0
- package/core/serializers/queue-serializer.ts +210 -0
- package/core/serializers/state-serializer.ts +136 -0
- package/core/utils/file-helper.ts +12 -0
- package/package.json +1 -1
- package/packages/web/app/api/projects/[id]/stats/route.ts +6 -29
- package/packages/web/app/project/[id]/page.tsx +34 -1
- package/packages/web/app/project/[id]/stats/page.tsx +11 -5
- package/packages/web/app/settings/page.tsx +2 -221
- package/packages/web/components/AppSidebar/AppSidebar.tsx +5 -3
- package/packages/web/components/BlockersCard/BlockersCard.tsx +67 -0
- package/packages/web/components/BlockersCard/BlockersCard.types.ts +11 -0
- package/packages/web/components/BlockersCard/index.ts +2 -0
- package/packages/web/components/CommandButton/CommandButton.tsx +10 -3
- package/packages/web/lib/projects.ts +28 -27
- package/packages/web/lib/services/projects.server.ts +25 -21
- package/packages/web/lib/services/stats.server.ts +355 -57
- package/packages/web/package.json +0 -2
- package/templates/commands/decision.md +226 -0
- package/templates/commands/done.md +100 -68
- package/templates/commands/feature.md +102 -103
- package/templates/commands/idea.md +41 -38
- package/templates/commands/now.md +94 -33
- package/templates/commands/pause.md +90 -30
- package/templates/commands/ship.md +179 -74
- package/templates/commands/sync.md +324 -200
- package/packages/web/app/api/migrate/route.ts +0 -46
- package/packages/web/app/api/settings/route.ts +0 -97
- package/packages/web/app/api/v2/projects/[id]/unified/route.ts +0 -57
- package/packages/web/lib/json-loader.ts +0 -630
- package/packages/web/lib/services/migration.server.ts +0 -600
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MD State Manager
|
|
3
|
+
*
|
|
4
|
+
* MD-First Architecture: Manages state via now.md.
|
|
5
|
+
* Source of truth is the markdown file, not JSON.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { MdBaseManager } from './md-base-manager'
|
|
9
|
+
import { parseState, serializeState } from '../serializers'
|
|
10
|
+
import type { StateJson, CurrentTask, PreviousTask } from '../schemas/state'
|
|
11
|
+
|
|
12
|
+
class MdStateManager extends MdBaseManager<StateJson> {
|
|
13
|
+
constructor() {
|
|
14
|
+
super('core/now.md')
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
protected getDefault(): StateJson {
|
|
18
|
+
return {
|
|
19
|
+
currentTask: null,
|
|
20
|
+
lastUpdated: ''
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
protected parse(content: string): StateJson {
|
|
25
|
+
return parseState(content)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
protected serialize(data: StateJson): string {
|
|
29
|
+
return serializeState(data)
|
|
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<StateJson> {
|
|
40
|
+
return this.update(projectId, (state) => ({
|
|
41
|
+
...state,
|
|
42
|
+
currentTask: task,
|
|
43
|
+
lastUpdated: new Date().toISOString()
|
|
44
|
+
}))
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async startTask(
|
|
48
|
+
projectId: string,
|
|
49
|
+
task: Omit<CurrentTask, 'startedAt'>
|
|
50
|
+
): Promise<StateJson> {
|
|
51
|
+
const currentTask: CurrentTask = {
|
|
52
|
+
...task,
|
|
53
|
+
startedAt: new Date().toISOString()
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return this.update(projectId, () => ({
|
|
57
|
+
currentTask,
|
|
58
|
+
lastUpdated: new Date().toISOString()
|
|
59
|
+
}))
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async completeTask(projectId: string): Promise<StateJson> {
|
|
63
|
+
const state = await this.read(projectId)
|
|
64
|
+
if (!state.currentTask) {
|
|
65
|
+
throw new Error('No active task to complete')
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return this.update(projectId, () => ({
|
|
69
|
+
currentTask: null,
|
|
70
|
+
lastUpdated: new Date().toISOString()
|
|
71
|
+
}))
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async pauseTask(projectId: string): Promise<StateJson> {
|
|
75
|
+
const state = await this.read(projectId)
|
|
76
|
+
if (!state.currentTask) {
|
|
77
|
+
throw new Error('No active task to pause')
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const previousTask: PreviousTask = {
|
|
81
|
+
id: state.currentTask.id,
|
|
82
|
+
description: state.currentTask.description,
|
|
83
|
+
status: 'paused',
|
|
84
|
+
startedAt: state.currentTask.startedAt,
|
|
85
|
+
pausedAt: new Date().toISOString()
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return this.update(projectId, () => ({
|
|
89
|
+
currentTask: null,
|
|
90
|
+
previousTask,
|
|
91
|
+
lastUpdated: new Date().toISOString()
|
|
92
|
+
}))
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async resumeTask(projectId: string): Promise<StateJson> {
|
|
96
|
+
const state = await this.read(projectId)
|
|
97
|
+
if (!state.previousTask) {
|
|
98
|
+
throw new Error('No paused task to resume')
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const currentTask: CurrentTask = {
|
|
102
|
+
id: state.previousTask.id,
|
|
103
|
+
description: state.previousTask.description,
|
|
104
|
+
startedAt: new Date().toISOString(), // Reset start time
|
|
105
|
+
sessionId: `sess_${Date.now()}`
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return this.update(projectId, () => ({
|
|
109
|
+
currentTask,
|
|
110
|
+
previousTask: null,
|
|
111
|
+
lastUpdated: new Date().toISOString()
|
|
112
|
+
}))
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async clearTask(projectId: string): Promise<StateJson> {
|
|
116
|
+
return this.update(projectId, () => ({
|
|
117
|
+
currentTask: null,
|
|
118
|
+
previousTask: null,
|
|
119
|
+
lastUpdated: new Date().toISOString()
|
|
120
|
+
}))
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Check if there's an active or paused task
|
|
125
|
+
*/
|
|
126
|
+
async hasTask(projectId: string): Promise<boolean> {
|
|
127
|
+
const state = await this.read(projectId)
|
|
128
|
+
return state.currentTask !== null || state.previousTask !== null
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export const mdStateManager = new MdStateManager()
|
|
133
|
+
export default mdStateManager
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Serializers Index
|
|
3
|
+
*
|
|
4
|
+
* MD-First Architecture: These serializers convert between TypeScript schemas and MD format.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// State (now.md)
|
|
8
|
+
export {
|
|
9
|
+
parseState,
|
|
10
|
+
serializeState,
|
|
11
|
+
createCurrentTaskMd,
|
|
12
|
+
createEmptyStateMd
|
|
13
|
+
} from './state-serializer'
|
|
14
|
+
|
|
15
|
+
// Queue (next.md)
|
|
16
|
+
export {
|
|
17
|
+
parseQueue,
|
|
18
|
+
serializeQueue,
|
|
19
|
+
createEmptyQueueMd
|
|
20
|
+
} from './queue-serializer'
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Queue Serializer
|
|
3
|
+
*
|
|
4
|
+
* Parses and serializes next.md for task queue.
|
|
5
|
+
*
|
|
6
|
+
* MD Format (next.md):
|
|
7
|
+
* ```
|
|
8
|
+
* # Priority Queue
|
|
9
|
+
*
|
|
10
|
+
* > Tasks ready to start
|
|
11
|
+
*
|
|
12
|
+
* ## Active Tasks
|
|
13
|
+
* 1. [ ] Task description @agent (from: Feature Name)
|
|
14
|
+
* 2. [x] Completed task ✅
|
|
15
|
+
*
|
|
16
|
+
* ## Previously Active
|
|
17
|
+
* - [ ] Paused task
|
|
18
|
+
*
|
|
19
|
+
* ## Backlog
|
|
20
|
+
* - [ ] 🐛 [HIGH] Bug description
|
|
21
|
+
* - [ ] Feature task
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import type { QueueJson, QueueTask, Priority, TaskType, TaskSection } from '../schemas/state'
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Parse next.md content to QueueJson
|
|
29
|
+
*/
|
|
30
|
+
export function parseQueue(content: string): QueueJson {
|
|
31
|
+
if (!content || !content.trim()) {
|
|
32
|
+
return { tasks: [], lastUpdated: '' }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const lines = content.split('\n')
|
|
36
|
+
const tasks: QueueTask[] = []
|
|
37
|
+
let currentSection: TaskSection = 'active'
|
|
38
|
+
let taskIndex = 0
|
|
39
|
+
|
|
40
|
+
for (const line of lines) {
|
|
41
|
+
// Detect section headers
|
|
42
|
+
if (line.match(/^##\s*Active/i)) {
|
|
43
|
+
currentSection = 'active'
|
|
44
|
+
continue
|
|
45
|
+
}
|
|
46
|
+
if (line.match(/^##\s*Previously/i)) {
|
|
47
|
+
currentSection = 'previously_active'
|
|
48
|
+
continue
|
|
49
|
+
}
|
|
50
|
+
if (line.match(/^##\s*Backlog/i)) {
|
|
51
|
+
currentSection = 'backlog'
|
|
52
|
+
continue
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Parse task lines: "1. [ ] Task" or "- [ ] Task" or "- [x] Task"
|
|
56
|
+
const taskMatch = line.match(/^(?:\d+\.|[-*])\s*\[([\sx])\]\s*(.+)$/i)
|
|
57
|
+
if (!taskMatch) continue
|
|
58
|
+
|
|
59
|
+
const isCompleted = taskMatch[1].toLowerCase() === 'x'
|
|
60
|
+
let taskText = taskMatch[2].trim()
|
|
61
|
+
|
|
62
|
+
// Extract agent: @fe, @be, @fe+be
|
|
63
|
+
const agentMatch = taskText.match(/@(\w+(?:\+\w+)?)/)
|
|
64
|
+
const agent = agentMatch ? agentMatch[1] : undefined
|
|
65
|
+
taskText = taskText.replace(/@\w+(?:\+\w+)?/, '').trim()
|
|
66
|
+
|
|
67
|
+
// Extract origin feature: (from: Feature Name)
|
|
68
|
+
const fromMatch = taskText.match(/\(from:\s*([^)]+)\)/)
|
|
69
|
+
const originFeature = fromMatch ? fromMatch[1].trim() : undefined
|
|
70
|
+
taskText = taskText.replace(/\(from:\s*[^)]+\)/, '').trim()
|
|
71
|
+
|
|
72
|
+
// Detect task type from emoji/prefix
|
|
73
|
+
let type: TaskType = 'feature'
|
|
74
|
+
let priority: Priority = 'medium'
|
|
75
|
+
|
|
76
|
+
if (taskText.includes('🐛') || taskText.toLowerCase().includes('bug')) {
|
|
77
|
+
type = 'bug'
|
|
78
|
+
taskText = taskText.replace('🐛', '').trim()
|
|
79
|
+
}
|
|
80
|
+
if (taskText.includes('🔧') || taskText.toLowerCase().includes('fix')) {
|
|
81
|
+
type = 'improvement'
|
|
82
|
+
}
|
|
83
|
+
if (taskText.includes('♻️') || taskText.toLowerCase().includes('refactor')) {
|
|
84
|
+
type = 'chore'
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Extract priority: [HIGH], [MEDIUM], [LOW], [CRITICAL]
|
|
88
|
+
const priorityMatch = taskText.match(/\[(HIGH|MEDIUM|LOW|CRITICAL)\]/i)
|
|
89
|
+
if (priorityMatch) {
|
|
90
|
+
priority = priorityMatch[1].toLowerCase() as Priority
|
|
91
|
+
taskText = taskText.replace(/\[(HIGH|MEDIUM|LOW|CRITICAL)\]/i, '').trim()
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Remove checkmarks and clean up
|
|
95
|
+
taskText = taskText.replace(/✅/g, '').trim()
|
|
96
|
+
|
|
97
|
+
// Skip empty descriptions
|
|
98
|
+
if (!taskText) continue
|
|
99
|
+
|
|
100
|
+
const task: QueueTask = {
|
|
101
|
+
id: `task_${Date.now()}_${taskIndex++}`,
|
|
102
|
+
description: taskText,
|
|
103
|
+
priority,
|
|
104
|
+
type,
|
|
105
|
+
completed: isCompleted,
|
|
106
|
+
createdAt: new Date().toISOString(),
|
|
107
|
+
section: currentSection
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (agent) task.agent = agent
|
|
111
|
+
if (originFeature) task.originFeature = originFeature
|
|
112
|
+
if (isCompleted) task.completedAt = new Date().toISOString()
|
|
113
|
+
|
|
114
|
+
tasks.push(task)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Extract updated date
|
|
118
|
+
const updatedMatch = content.match(/_Updated:\s*(\d{4}-\d{2}-\d{2})/)
|
|
119
|
+
const lastUpdated = updatedMatch ? updatedMatch[1] : new Date().toISOString().split('T')[0]
|
|
120
|
+
|
|
121
|
+
return { tasks, lastUpdated }
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Serialize QueueJson to next.md format
|
|
126
|
+
*/
|
|
127
|
+
export function serializeQueue(data: QueueJson): string {
|
|
128
|
+
const lines: string[] = [
|
|
129
|
+
'# Priority Queue',
|
|
130
|
+
'',
|
|
131
|
+
'> Tasks ready to start (max 100)',
|
|
132
|
+
'> Auto-updated by prjct',
|
|
133
|
+
''
|
|
134
|
+
]
|
|
135
|
+
|
|
136
|
+
// Group tasks by section
|
|
137
|
+
const active = data.tasks.filter(t => t.section === 'active')
|
|
138
|
+
const previouslyActive = data.tasks.filter(t => t.section === 'previously_active')
|
|
139
|
+
const backlog = data.tasks.filter(t => t.section === 'backlog')
|
|
140
|
+
|
|
141
|
+
// Active Tasks
|
|
142
|
+
if (active.length > 0) {
|
|
143
|
+
lines.push('## Active Tasks', '')
|
|
144
|
+
active.forEach((task, i) => {
|
|
145
|
+
lines.push(formatTask(task, i + 1, true))
|
|
146
|
+
})
|
|
147
|
+
lines.push('')
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Previously Active
|
|
151
|
+
if (previouslyActive.length > 0) {
|
|
152
|
+
lines.push('## Previously Active', '')
|
|
153
|
+
previouslyActive.forEach(task => {
|
|
154
|
+
lines.push(formatTask(task, 0, false))
|
|
155
|
+
})
|
|
156
|
+
lines.push('')
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Backlog
|
|
160
|
+
lines.push('---', '', '## Backlog', '')
|
|
161
|
+
if (backlog.length > 0) {
|
|
162
|
+
backlog.forEach(task => {
|
|
163
|
+
lines.push(formatTask(task, 0, false))
|
|
164
|
+
})
|
|
165
|
+
} else {
|
|
166
|
+
lines.push('_No backlog items_')
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
lines.push('')
|
|
170
|
+
lines.push('---', '')
|
|
171
|
+
lines.push(`_Updated: ${data.lastUpdated || new Date().toISOString().split('T')[0]}_`)
|
|
172
|
+
|
|
173
|
+
return lines.join('\n')
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Format a single task as markdown
|
|
178
|
+
*/
|
|
179
|
+
function formatTask(task: QueueTask, num: number, numbered: boolean): string {
|
|
180
|
+
const checkbox = task.completed ? '[x]' : '[ ]'
|
|
181
|
+
const prefix = numbered && num > 0 ? `${num}.` : '-'
|
|
182
|
+
|
|
183
|
+
let text = task.description
|
|
184
|
+
|
|
185
|
+
// Add emoji for type
|
|
186
|
+
if (task.type === 'bug') text = `🐛 ${text}`
|
|
187
|
+
|
|
188
|
+
// Add priority tag for high/critical
|
|
189
|
+
if (task.priority === 'high' || task.priority === 'critical') {
|
|
190
|
+
text = `[${task.priority.toUpperCase()}] ${text}`
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Add agent
|
|
194
|
+
if (task.agent) text = `${text} @${task.agent}`
|
|
195
|
+
|
|
196
|
+
// Add origin feature
|
|
197
|
+
if (task.originFeature) text = `${text} (from: ${task.originFeature})`
|
|
198
|
+
|
|
199
|
+
// Add checkmark for completed
|
|
200
|
+
if (task.completed) text = `${text} ✅`
|
|
201
|
+
|
|
202
|
+
return `${prefix} ${checkbox} ${text}`
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Quick helpers
|
|
207
|
+
*/
|
|
208
|
+
export function createEmptyQueueMd(): string {
|
|
209
|
+
return serializeQueue({ tasks: [], lastUpdated: new Date().toISOString().split('T')[0] })
|
|
210
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* State Serializer
|
|
3
|
+
*
|
|
4
|
+
* Parses and serializes now.md for current task state.
|
|
5
|
+
*
|
|
6
|
+
* MD Format (now.md):
|
|
7
|
+
* ```
|
|
8
|
+
* # NOW
|
|
9
|
+
*
|
|
10
|
+
* **Task description here**
|
|
11
|
+
*
|
|
12
|
+
* Started: 2025-12-10T10:00:00.000Z
|
|
13
|
+
* Session: sess_abc123
|
|
14
|
+
* Feature: feat_xyz789
|
|
15
|
+
* Agent: fe
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import type { StateJson, CurrentTask, PreviousTask } from '../schemas/state'
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Parse now.md content to StateJson
|
|
23
|
+
*/
|
|
24
|
+
export function parseState(content: string): StateJson {
|
|
25
|
+
if (!content || !content.trim()) {
|
|
26
|
+
return { currentTask: null, lastUpdated: '' }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const lines = content.split('\n')
|
|
30
|
+
let currentTask: CurrentTask | null = null
|
|
31
|
+
let previousTask: PreviousTask | null = null
|
|
32
|
+
|
|
33
|
+
// Find task description (bold line after # NOW)
|
|
34
|
+
let description = ''
|
|
35
|
+
for (const line of lines) {
|
|
36
|
+
const boldMatch = line.match(/^\*\*(.+)\*\*$/)
|
|
37
|
+
if (boldMatch) {
|
|
38
|
+
description = boldMatch[1].trim()
|
|
39
|
+
break
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (!description || description.toLowerCase().includes('no active task')) {
|
|
44
|
+
return { currentTask: null, lastUpdated: '' }
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Extract metadata
|
|
48
|
+
const startedMatch = content.match(/Started:\s*(.+)/i)
|
|
49
|
+
const sessionMatch = content.match(/Session:\s*(.+)/i)
|
|
50
|
+
const featureMatch = content.match(/Feature:\s*(.+)/i)
|
|
51
|
+
const idMatch = content.match(/ID:\s*(.+)/i)
|
|
52
|
+
const agentMatch = content.match(/Agent:\s*(.+)/i)
|
|
53
|
+
const pausedMatch = content.match(/Paused:\s*(.+)/i)
|
|
54
|
+
|
|
55
|
+
const id = idMatch ? idMatch[1].trim() : `task_${Date.now()}`
|
|
56
|
+
const startedAt = startedMatch ? startedMatch[1].trim() : new Date().toISOString()
|
|
57
|
+
const sessionId = sessionMatch ? sessionMatch[1].trim() : `sess_${Date.now()}`
|
|
58
|
+
|
|
59
|
+
if (pausedMatch) {
|
|
60
|
+
// This is a paused task
|
|
61
|
+
previousTask = {
|
|
62
|
+
id,
|
|
63
|
+
description,
|
|
64
|
+
status: 'paused',
|
|
65
|
+
startedAt,
|
|
66
|
+
pausedAt: pausedMatch[1].trim()
|
|
67
|
+
}
|
|
68
|
+
} else {
|
|
69
|
+
// Active task
|
|
70
|
+
currentTask = {
|
|
71
|
+
id,
|
|
72
|
+
description,
|
|
73
|
+
startedAt,
|
|
74
|
+
sessionId
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (featureMatch) {
|
|
78
|
+
currentTask.featureId = featureMatch[1].trim()
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
currentTask,
|
|
84
|
+
previousTask,
|
|
85
|
+
lastUpdated: startedAt
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Serialize StateJson to now.md format
|
|
91
|
+
*/
|
|
92
|
+
export function serializeState(data: StateJson): string {
|
|
93
|
+
const lines: string[] = ['# NOW', '']
|
|
94
|
+
|
|
95
|
+
if (!data.currentTask && !data.previousTask) {
|
|
96
|
+
lines.push('_No active task_', '')
|
|
97
|
+
lines.push('Use `/p:now` to start working on something.')
|
|
98
|
+
return lines.join('\n')
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const task = data.currentTask || data.previousTask
|
|
102
|
+
|
|
103
|
+
if (task) {
|
|
104
|
+
lines.push(`**${task.description}**`, '')
|
|
105
|
+
|
|
106
|
+
if ('pausedAt' in task && task.pausedAt) {
|
|
107
|
+
lines.push(`Started: ${task.startedAt}`)
|
|
108
|
+
lines.push(`Paused: ${task.pausedAt}`)
|
|
109
|
+
} else if (data.currentTask) {
|
|
110
|
+
lines.push(`Started: ${data.currentTask.startedAt}`)
|
|
111
|
+
lines.push(`Session: ${data.currentTask.sessionId}`)
|
|
112
|
+
if (data.currentTask.featureId) {
|
|
113
|
+
lines.push(`Feature: ${data.currentTask.featureId}`)
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return lines.join('\n')
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Quick helpers for common operations
|
|
123
|
+
*/
|
|
124
|
+
export function createCurrentTaskMd(task: CurrentTask): string {
|
|
125
|
+
return serializeState({
|
|
126
|
+
currentTask: task,
|
|
127
|
+
lastUpdated: task.startedAt
|
|
128
|
+
})
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function createEmptyStateMd(): string {
|
|
132
|
+
return serializeState({
|
|
133
|
+
currentTask: null,
|
|
134
|
+
lastUpdated: ''
|
|
135
|
+
})
|
|
136
|
+
}
|
|
@@ -66,6 +66,17 @@ export async function writeFile(filePath: string, content: string): Promise<void
|
|
|
66
66
|
await fs.writeFile(filePath, content, 'utf-8')
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
+
/**
|
|
70
|
+
* Atomic write - writes to temp file then renames (prevents partial writes)
|
|
71
|
+
*/
|
|
72
|
+
export async function atomicWrite(filePath: string, content: string): Promise<void> {
|
|
73
|
+
const dir = path.dirname(filePath)
|
|
74
|
+
await fs.mkdir(dir, { recursive: true })
|
|
75
|
+
const tempPath = `${filePath}.${Date.now()}.tmp`
|
|
76
|
+
await fs.writeFile(tempPath, content, 'utf-8')
|
|
77
|
+
await fs.rename(tempPath, filePath)
|
|
78
|
+
}
|
|
79
|
+
|
|
69
80
|
/**
|
|
70
81
|
* Append to text file
|
|
71
82
|
*/
|
|
@@ -245,6 +256,7 @@ export default {
|
|
|
245
256
|
writeJson,
|
|
246
257
|
readFile,
|
|
247
258
|
writeFile,
|
|
259
|
+
atomicWrite,
|
|
248
260
|
fileExists,
|
|
249
261
|
ensureDir,
|
|
250
262
|
deleteFile,
|
package/package.json
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { NextRequest, NextResponse } from 'next/server'
|
|
2
|
-
import { loadUnifiedJsonData, hasJsonState } from '@/lib/json-loader'
|
|
3
2
|
import { getProjectStats, getRawProjectFiles } from '@/lib/parse-prjct-files'
|
|
4
3
|
|
|
4
|
+
/**
|
|
5
|
+
* GET /api/projects/[id]/stats
|
|
6
|
+
*
|
|
7
|
+
* MD-First Architecture: Returns stats parsed from MD files.
|
|
8
|
+
*/
|
|
5
9
|
export async function GET(
|
|
6
10
|
request: NextRequest,
|
|
7
11
|
{ params }: { params: Promise<{ id: string }> }
|
|
@@ -16,33 +20,6 @@ export async function GET(
|
|
|
16
20
|
}
|
|
17
21
|
|
|
18
22
|
try {
|
|
19
|
-
// Check if JSON files exist (new format)
|
|
20
|
-
const hasJson = await hasJsonState(projectId)
|
|
21
|
-
|
|
22
|
-
if (hasJson) {
|
|
23
|
-
// Use new JSON loader (fast path)
|
|
24
|
-
const jsonData = await loadUnifiedJsonData(projectId)
|
|
25
|
-
|
|
26
|
-
// Convert to stats-compatible format using new architecture
|
|
27
|
-
return NextResponse.json({
|
|
28
|
-
success: true,
|
|
29
|
-
version: 'v2',
|
|
30
|
-
data: {
|
|
31
|
-
currentTask: jsonData.state?.currentTask || null,
|
|
32
|
-
queue: jsonData.queue?.tasks?.filter(t => !t.completed) || [],
|
|
33
|
-
metrics: jsonData.metrics || null,
|
|
34
|
-
agents: jsonData.agents,
|
|
35
|
-
ideas: jsonData.ideas?.ideas || [],
|
|
36
|
-
roadmap: jsonData.roadmap?.features || [],
|
|
37
|
-
shipped: jsonData.shipped?.items || [],
|
|
38
|
-
analysis: jsonData.analysis,
|
|
39
|
-
outcomes: jsonData.outcomes,
|
|
40
|
-
insights: jsonData.insights
|
|
41
|
-
}
|
|
42
|
-
})
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// Fallback to legacy markdown parsing
|
|
46
23
|
const [stats, raw] = await Promise.all([
|
|
47
24
|
getProjectStats(projectId),
|
|
48
25
|
getRawProjectFiles(projectId)
|
|
@@ -50,7 +27,7 @@ export async function GET(
|
|
|
50
27
|
|
|
51
28
|
return NextResponse.json({
|
|
52
29
|
success: true,
|
|
53
|
-
version: '
|
|
30
|
+
version: 'md-first',
|
|
54
31
|
data: stats,
|
|
55
32
|
raw
|
|
56
33
|
})
|
|
@@ -44,7 +44,8 @@ import {
|
|
|
44
44
|
Undo2,
|
|
45
45
|
Redo2,
|
|
46
46
|
Command,
|
|
47
|
-
X
|
|
47
|
+
X,
|
|
48
|
+
RefreshCw
|
|
48
49
|
} from 'lucide-react'
|
|
49
50
|
import { cn } from '@/lib/utils'
|
|
50
51
|
|
|
@@ -111,6 +112,15 @@ function CommandSidebarContent({
|
|
|
111
112
|
onCommandClick?.()
|
|
112
113
|
}}
|
|
113
114
|
/>
|
|
115
|
+
{/* Sync button - prominent, always visible */}
|
|
116
|
+
<CommandButton
|
|
117
|
+
cmd="p. sync"
|
|
118
|
+
icon={RefreshCw}
|
|
119
|
+
tip="Sync"
|
|
120
|
+
disabled={!isActiveConnected}
|
|
121
|
+
onClick={() => handleCommand('p. sync')}
|
|
122
|
+
variant="primary"
|
|
123
|
+
/>
|
|
114
124
|
<div className="border-b border-border w-8 my-2 mx-auto" />
|
|
115
125
|
|
|
116
126
|
{COMMAND_GROUPS.map((group, groupIndex) => (
|
|
@@ -268,6 +278,29 @@ function ProjectPageContent({ projectId, project }: { projectId: string; project
|
|
|
268
278
|
<span className="text-xs text-muted-foreground">Stats</span>
|
|
269
279
|
</button>
|
|
270
280
|
|
|
281
|
+
{/* Sync button - prominent */}
|
|
282
|
+
<button
|
|
283
|
+
onClick={() => {
|
|
284
|
+
sendCommandToActive('p. sync')
|
|
285
|
+
setCommandSheetOpen(false)
|
|
286
|
+
}}
|
|
287
|
+
disabled={!isActiveConnected}
|
|
288
|
+
className={cn(
|
|
289
|
+
"flex flex-col items-center gap-1.5 p-3 rounded-lg transition-colors",
|
|
290
|
+
isActiveConnected
|
|
291
|
+
? "hover:bg-primary/10"
|
|
292
|
+
: "opacity-50 cursor-not-allowed"
|
|
293
|
+
)}
|
|
294
|
+
>
|
|
295
|
+
<div className={cn(
|
|
296
|
+
"h-10 w-10 rounded-full flex items-center justify-center",
|
|
297
|
+
isActiveConnected ? "bg-primary text-primary-foreground" : "bg-muted"
|
|
298
|
+
)}>
|
|
299
|
+
<RefreshCw className="h-5 w-5" />
|
|
300
|
+
</div>
|
|
301
|
+
<span className="text-xs text-primary font-medium">Sync</span>
|
|
302
|
+
</button>
|
|
303
|
+
|
|
271
304
|
{WORKFLOW_COMMANDS.map(({ cmd, icon: Icon, tip }) => (
|
|
272
305
|
<button
|
|
273
306
|
key={cmd}
|