prjct-cli 0.60.2 → 0.61.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 +36 -0
- package/core/services/local-state-generator.ts +158 -0
- package/core/services/sync-service.ts +11 -0
- package/core/storage/state-storage.ts +3 -0
- package/dist/bin/prjct.mjs +428 -304
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,41 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.61.0] - 2026-02-05
|
|
4
|
+
|
|
5
|
+
### Features
|
|
6
|
+
|
|
7
|
+
- add .prjct-state.md local state file for persistence (PRJ-112) (#102)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
## [0.61.0] - 2026-02-05
|
|
11
|
+
|
|
12
|
+
### Features
|
|
13
|
+
|
|
14
|
+
- **Local state file (PRJ-112)**: New `.prjct-state.md` file generated in project root for local persistence
|
|
15
|
+
|
|
16
|
+
### Implementation Details
|
|
17
|
+
|
|
18
|
+
Created `LocalStateGenerator` service that generates a markdown file showing current task state. Integrated via write-through pattern - `StateStorage.write()` now also generates the local state file. Also hooks into `sync-service.ts` for state.json updates during sync.
|
|
19
|
+
|
|
20
|
+
### Learnings
|
|
21
|
+
|
|
22
|
+
- Write-through pattern: JSON storage triggers MD generation automatically
|
|
23
|
+
- State can be written from multiple entry points (storage class + sync service) - need hooks in both places
|
|
24
|
+
|
|
25
|
+
### Test Plan
|
|
26
|
+
|
|
27
|
+
#### For QA
|
|
28
|
+
1. Run `prjct sync` on any project - verify `.prjct-state.md` is generated in project root
|
|
29
|
+
2. Start a task with `p. task "test"` - verify `.prjct-state.md` updates with task info
|
|
30
|
+
3. Check that subtasks, progress, and status are displayed correctly
|
|
31
|
+
4. Verify the file has "DO NOT EDIT" header comment
|
|
32
|
+
|
|
33
|
+
#### For Users
|
|
34
|
+
- New `.prjct-state.md` file in project root shows current task state
|
|
35
|
+
- Automatic - file updates whenever prjct state changes
|
|
36
|
+
- No breaking changes
|
|
37
|
+
|
|
38
|
+
|
|
3
39
|
## [0.60.2] - 2026-02-05
|
|
4
40
|
|
|
5
41
|
### Performance
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local State Generator
|
|
3
|
+
*
|
|
4
|
+
* Generates .prjct-state.md in the project root for local persistence.
|
|
5
|
+
* This provides a quick reference to current task state without needing
|
|
6
|
+
* to access global storage.
|
|
7
|
+
*
|
|
8
|
+
* @see PRJ-112
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import fs from 'node:fs/promises'
|
|
12
|
+
import path from 'node:path'
|
|
13
|
+
import type { StateJson } from '../schemas/state'
|
|
14
|
+
import { isNotFoundError } from '../types/fs'
|
|
15
|
+
|
|
16
|
+
const LOCAL_STATE_FILENAME = '.prjct-state.md'
|
|
17
|
+
|
|
18
|
+
// Extended runtime types (state.json has fields not in strict schema)
|
|
19
|
+
interface RuntimeTask {
|
|
20
|
+
description: string
|
|
21
|
+
startedAt: string
|
|
22
|
+
linearId?: string
|
|
23
|
+
branch?: string
|
|
24
|
+
status?: string
|
|
25
|
+
subtasks?: Array<{
|
|
26
|
+
description: string
|
|
27
|
+
status: string
|
|
28
|
+
}>
|
|
29
|
+
currentSubtaskIndex?: number
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface RuntimePreviousTask {
|
|
33
|
+
description: string
|
|
34
|
+
status: string
|
|
35
|
+
prUrl?: string
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
class LocalStateGenerator {
|
|
39
|
+
/**
|
|
40
|
+
* Generate .prjct-state.md in the project root
|
|
41
|
+
*/
|
|
42
|
+
async generate(projectPath: string, state: StateJson): Promise<void> {
|
|
43
|
+
const filePath = path.join(projectPath, LOCAL_STATE_FILENAME)
|
|
44
|
+
const content = this.toMarkdown(state)
|
|
45
|
+
await fs.writeFile(filePath, content, 'utf-8')
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Remove local state file
|
|
50
|
+
*/
|
|
51
|
+
async remove(projectPath: string): Promise<void> {
|
|
52
|
+
const filePath = path.join(projectPath, LOCAL_STATE_FILENAME)
|
|
53
|
+
try {
|
|
54
|
+
await fs.unlink(filePath)
|
|
55
|
+
} catch (error) {
|
|
56
|
+
if (!isNotFoundError(error)) throw error
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Check if local state file exists
|
|
62
|
+
*/
|
|
63
|
+
async exists(projectPath: string): Promise<boolean> {
|
|
64
|
+
const filePath = path.join(projectPath, LOCAL_STATE_FILENAME)
|
|
65
|
+
try {
|
|
66
|
+
await fs.access(filePath)
|
|
67
|
+
return true
|
|
68
|
+
} catch {
|
|
69
|
+
return false
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Convert state to markdown format
|
|
75
|
+
* Note: Uses runtime types since state.json has fields not in strict Zod schema
|
|
76
|
+
*/
|
|
77
|
+
private toMarkdown(state: StateJson): string {
|
|
78
|
+
const lines: string[] = [
|
|
79
|
+
'<!-- Auto-generated by prjct - DO NOT EDIT -->',
|
|
80
|
+
'<!-- This file provides local state persistence for AI tools -->',
|
|
81
|
+
'',
|
|
82
|
+
'# prjct State',
|
|
83
|
+
'',
|
|
84
|
+
]
|
|
85
|
+
|
|
86
|
+
if (state.currentTask) {
|
|
87
|
+
// Cast to runtime type (state.json has additional fields)
|
|
88
|
+
const task = state.currentTask as unknown as RuntimeTask
|
|
89
|
+
lines.push('## Current Task')
|
|
90
|
+
lines.push('')
|
|
91
|
+
lines.push(`**${task.description}**`)
|
|
92
|
+
lines.push('')
|
|
93
|
+
lines.push(`- Started: ${task.startedAt}`)
|
|
94
|
+
if (task.linearId) {
|
|
95
|
+
lines.push(`- Linear: ${task.linearId}`)
|
|
96
|
+
}
|
|
97
|
+
if (task.branch) {
|
|
98
|
+
lines.push(`- Branch: ${task.branch}`)
|
|
99
|
+
}
|
|
100
|
+
lines.push(`- Status: ${task.status || 'active'}`)
|
|
101
|
+
lines.push('')
|
|
102
|
+
|
|
103
|
+
// Subtasks
|
|
104
|
+
if (task.subtasks && task.subtasks.length > 0) {
|
|
105
|
+
lines.push('### Subtasks')
|
|
106
|
+
lines.push('')
|
|
107
|
+
|
|
108
|
+
task.subtasks.forEach((subtask, index) => {
|
|
109
|
+
const statusIcon =
|
|
110
|
+
subtask.status === 'completed' ? '✅' : subtask.status === 'in_progress' ? '▶️' : '⏳'
|
|
111
|
+
const isActive = index === task.currentSubtaskIndex ? ' ← **Active**' : ''
|
|
112
|
+
lines.push(`${index + 1}. ${statusIcon} ${subtask.description}${isActive}`)
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
lines.push('')
|
|
116
|
+
|
|
117
|
+
// Progress
|
|
118
|
+
const completed = task.subtasks.filter((s) => s.status === 'completed').length
|
|
119
|
+
const total = task.subtasks.length
|
|
120
|
+
const percentage = Math.round((completed / total) * 100)
|
|
121
|
+
lines.push(`**Progress**: ${completed}/${total} (${percentage}%)`)
|
|
122
|
+
lines.push('')
|
|
123
|
+
}
|
|
124
|
+
} else {
|
|
125
|
+
lines.push('*No active task*')
|
|
126
|
+
lines.push('')
|
|
127
|
+
lines.push('Start a task with `p. task "description"`')
|
|
128
|
+
lines.push('')
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Previous task info
|
|
132
|
+
if (state.previousTask) {
|
|
133
|
+
// Cast to runtime type
|
|
134
|
+
const prevTask = state.previousTask as unknown as RuntimePreviousTask
|
|
135
|
+
lines.push('---')
|
|
136
|
+
lines.push('')
|
|
137
|
+
lines.push('## Previous Task')
|
|
138
|
+
lines.push('')
|
|
139
|
+
lines.push(`**${prevTask.description}**`)
|
|
140
|
+
lines.push('')
|
|
141
|
+
lines.push(`- Status: ${prevTask.status}`)
|
|
142
|
+
if (prevTask.prUrl) {
|
|
143
|
+
lines.push(`- PR: ${prevTask.prUrl}`)
|
|
144
|
+
}
|
|
145
|
+
lines.push('')
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Footer
|
|
149
|
+
lines.push('---')
|
|
150
|
+
lines.push(`*Last updated: ${state.lastUpdated || new Date().toISOString()}*`)
|
|
151
|
+
lines.push('')
|
|
152
|
+
|
|
153
|
+
return lines.join('\n')
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export const localStateGenerator = new LocalStateGenerator()
|
|
158
|
+
export default localStateGenerator
|
|
@@ -33,6 +33,7 @@ import { metricsStorage } from '../storage/metrics-storage'
|
|
|
33
33
|
import dateHelper from '../utils/date-helper'
|
|
34
34
|
import { ContextFileGenerator } from './context-generator'
|
|
35
35
|
import type { SyncDiff } from './diff-generator'
|
|
36
|
+
import { localStateGenerator } from './local-state-generator'
|
|
36
37
|
import { type StackDetection, StackDetector } from './stack-detector'
|
|
37
38
|
|
|
38
39
|
const execAsync = promisify(exec)
|
|
@@ -826,6 +827,16 @@ You are the ${name} expert for this project. Apply best practices for the detect
|
|
|
826
827
|
}
|
|
827
828
|
|
|
828
829
|
await fs.writeFile(statePath, JSON.stringify(state, null, 2), 'utf-8')
|
|
830
|
+
|
|
831
|
+
// Also generate local .prjct-state.md (PRJ-112)
|
|
832
|
+
try {
|
|
833
|
+
await localStateGenerator.generate(
|
|
834
|
+
this.projectPath,
|
|
835
|
+
state as import('../schemas/state').StateJson
|
|
836
|
+
)
|
|
837
|
+
} catch {
|
|
838
|
+
// Silently fail - local state is optional
|
|
839
|
+
}
|
|
829
840
|
}
|
|
830
841
|
|
|
831
842
|
// ==========================================================================
|
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Manages current task state via storage/state.json
|
|
5
5
|
* Generates context/now.md for Claude
|
|
6
|
+
*
|
|
7
|
+
* Note: Local .prjct-state.md is generated by sync-service which has access
|
|
8
|
+
* to projectPath. This class only has projectId.
|
|
6
9
|
*/
|
|
7
10
|
|
|
8
11
|
import { generateUUID } from '../schemas'
|