prjct-cli 0.13.2 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +106 -0
- package/bin/prjct +10 -13
- package/core/agentic/memory-system/semantic-memories.ts +2 -1
- package/core/agentic/plan-mode/plan-mode.ts +2 -1
- package/core/agentic/prompt-builder.ts +22 -43
- package/core/agentic/services.ts +5 -5
- package/core/agentic/smart-context.ts +7 -2
- package/core/command-registry/core-commands.ts +54 -29
- package/core/command-registry/optional-commands.ts +64 -0
- package/core/command-registry/setup-commands.ts +18 -3
- package/core/commands/analysis.ts +21 -68
- package/core/commands/analytics.ts +247 -213
- package/core/commands/base.ts +1 -1
- package/core/commands/index.ts +41 -36
- package/core/commands/maintenance.ts +300 -31
- package/core/commands/planning.ts +233 -22
- package/core/commands/setup.ts +3 -8
- package/core/commands/shipping.ts +14 -18
- package/core/commands/types.ts +8 -6
- package/core/commands/workflow.ts +105 -100
- package/core/context/generator.ts +317 -0
- package/core/context-sync.ts +7 -350
- package/core/data/index.ts +13 -32
- package/core/data/md-ideas-manager.ts +155 -0
- package/core/data/md-queue-manager.ts +4 -3
- package/core/data/md-shipped-manager.ts +90 -0
- package/core/data/md-state-manager.ts +11 -7
- package/core/domain/agent-generator.ts +23 -63
- package/core/events/index.ts +143 -0
- package/core/index.ts +17 -14
- package/core/infrastructure/capability-installer.ts +13 -149
- package/core/infrastructure/migrator/project-scanner.ts +2 -1
- package/core/infrastructure/path-manager.ts +4 -6
- package/core/infrastructure/setup.ts +3 -0
- package/core/infrastructure/uuid-migration.ts +750 -0
- package/core/outcomes/recorder.ts +2 -1
- package/core/plugin/loader.ts +4 -7
- package/core/plugin/registry.ts +3 -3
- package/core/schemas/index.ts +23 -25
- package/core/schemas/state.ts +1 -0
- package/core/serializers/ideas-serializer.ts +187 -0
- package/core/serializers/index.ts +16 -0
- package/core/serializers/shipped-serializer.ts +108 -0
- package/core/session/utils.ts +3 -9
- package/core/storage/ideas-storage.ts +273 -0
- package/core/storage/index.ts +204 -0
- package/core/storage/queue-storage.ts +297 -0
- package/core/storage/shipped-storage.ts +223 -0
- package/core/storage/state-storage.ts +235 -0
- package/core/storage/storage-manager.ts +175 -0
- package/package.json +1 -1
- package/packages/web/app/api/projects/[id]/momentum/route.ts +257 -0
- package/packages/web/app/api/sessions/current/route.ts +132 -0
- package/packages/web/app/api/sessions/history/route.ts +96 -14
- package/packages/web/app/globals.css +5 -0
- package/packages/web/app/layout.tsx +2 -0
- package/packages/web/app/project/[id]/code/layout.tsx +18 -0
- package/packages/web/app/project/[id]/code/page.tsx +408 -0
- package/packages/web/app/project/[id]/page.tsx +359 -389
- package/packages/web/app/project/[id]/reports/page.tsx +59 -0
- package/packages/web/app/project/[id]/reports/print/page.tsx +58 -0
- package/packages/web/components/ActivityTimeline/ActivityTimeline.tsx +0 -1
- package/packages/web/components/AgentsCard/AgentsCard.tsx +64 -34
- package/packages/web/components/AgentsCard/AgentsCard.types.ts +1 -0
- package/packages/web/components/AppSidebar/AppSidebar.tsx +135 -11
- package/packages/web/components/BentoCard/BentoCard.constants.ts +3 -3
- package/packages/web/components/BentoCard/BentoCard.tsx +2 -1
- package/packages/web/components/BentoGrid/BentoGrid.tsx +2 -2
- package/packages/web/components/BlockersCard/BlockersCard.tsx +65 -57
- package/packages/web/components/BlockersCard/BlockersCard.types.ts +1 -0
- package/packages/web/components/CommandBar/CommandBar.tsx +67 -0
- package/packages/web/components/CommandBar/index.ts +1 -0
- package/packages/web/components/DashboardContent/DashboardContent.tsx +35 -5
- package/packages/web/components/DateGroup/DateGroup.tsx +1 -1
- package/packages/web/components/EmptyState/EmptyState.tsx +39 -21
- package/packages/web/components/EmptyState/EmptyState.types.ts +1 -0
- package/packages/web/components/EventRow/EventRow.tsx +4 -4
- package/packages/web/components/EventRow/EventRow.utils.ts +3 -3
- package/packages/web/components/HeroSection/HeroSection.tsx +52 -15
- package/packages/web/components/HeroSection/HeroSection.types.ts +4 -4
- package/packages/web/components/HeroSection/HeroSection.utils.ts +7 -3
- package/packages/web/components/IdeasCard/IdeasCard.tsx +94 -27
- package/packages/web/components/IdeasCard/IdeasCard.types.ts +1 -0
- package/packages/web/components/MasonryGrid/MasonryGrid.tsx +18 -0
- package/packages/web/components/MasonryGrid/index.ts +1 -0
- package/packages/web/components/MomentumWidget/MomentumWidget.tsx +119 -0
- package/packages/web/components/MomentumWidget/MomentumWidget.types.ts +16 -0
- package/packages/web/components/MomentumWidget/index.ts +2 -0
- package/packages/web/components/NowCard/NowCard.tsx +81 -56
- package/packages/web/components/NowCard/NowCard.types.ts +1 -0
- package/packages/web/components/PageHeader/PageHeader.tsx +24 -0
- package/packages/web/components/PageHeader/index.ts +1 -0
- package/packages/web/components/ProgressRing/ProgressRing.constants.ts +2 -2
- package/packages/web/components/ProjectAvatar/ProjectAvatar.tsx +2 -2
- package/packages/web/components/ProjectColorDot/ProjectColorDot.tsx +37 -0
- package/packages/web/components/ProjectColorDot/index.ts +1 -0
- package/packages/web/components/ProjectSelectorModal/ProjectSelectorModal.tsx +104 -0
- package/packages/web/components/ProjectSelectorModal/index.ts +1 -0
- package/packages/web/components/Providers/Providers.tsx +4 -1
- package/packages/web/components/QueueCard/QueueCard.tsx +78 -25
- package/packages/web/components/QueueCard/QueueCard.types.ts +1 -0
- package/packages/web/components/QueueCard/QueueCard.utils.ts +3 -3
- package/packages/web/components/RecoverCard/RecoverCard.tsx +72 -0
- package/packages/web/components/RecoverCard/RecoverCard.types.ts +16 -0
- package/packages/web/components/RecoverCard/index.ts +2 -0
- package/packages/web/components/RoadmapCard/RoadmapCard.tsx +101 -33
- package/packages/web/components/RoadmapCard/RoadmapCard.types.ts +1 -0
- package/packages/web/components/ShipsCard/ShipsCard.tsx +71 -28
- package/packages/web/components/ShipsCard/ShipsCard.types.ts +2 -0
- package/packages/web/components/SparklineChart/SparklineChart.tsx +20 -18
- package/packages/web/components/StatsMasonry/StatsMasonry.tsx +95 -0
- package/packages/web/components/StatsMasonry/index.ts +1 -0
- package/packages/web/components/StreakCard/StreakCard.tsx +37 -35
- package/packages/web/components/TasksCounter/TasksCounter.tsx +1 -1
- package/packages/web/components/TechStackBadges/TechStackBadges.tsx +12 -4
- package/packages/web/components/TerminalDock/DockToggleTab.tsx +29 -0
- package/packages/web/components/TerminalDock/TerminalDock.tsx +386 -0
- package/packages/web/components/TerminalDock/TerminalDockTab.tsx +130 -0
- package/packages/web/components/TerminalDock/TerminalTabBar.tsx +142 -0
- package/packages/web/components/TerminalDock/index.ts +2 -0
- package/packages/web/components/VelocityBadge/VelocityBadge.tsx +8 -3
- package/packages/web/components/VelocityCard/VelocityCard.tsx +49 -47
- package/packages/web/components/WeeklyReports/PrintableReport.tsx +259 -0
- package/packages/web/components/WeeklyReports/ReportPreviewCard.tsx +187 -0
- package/packages/web/components/WeeklyReports/WeekCalendar.tsx +288 -0
- package/packages/web/components/WeeklyReports/WeeklyReports.tsx +149 -0
- package/packages/web/components/WeeklyReports/index.ts +4 -0
- package/packages/web/components/WeeklySparkline/WeeklySparkline.tsx +16 -4
- package/packages/web/components/WeeklySparkline/WeeklySparkline.types.ts +1 -0
- package/packages/web/components/charts/SessionsChart.tsx +6 -3
- package/packages/web/components/ui/dialog.tsx +143 -0
- package/packages/web/components/ui/drawer.tsx +135 -0
- package/packages/web/components/ui/select.tsx +187 -0
- package/packages/web/context/GlobalTerminalContext.tsx +538 -0
- package/packages/web/lib/commands.ts +81 -0
- package/packages/web/lib/generate-week-report.ts +285 -0
- package/packages/web/lib/parse-prjct-files.ts +56 -55
- package/packages/web/lib/project-colors.ts +58 -0
- package/packages/web/lib/projects.ts +58 -5
- package/packages/web/lib/services/projects.server.ts +11 -1
- package/packages/web/next-env.d.ts +1 -1
- package/packages/web/package.json +5 -1
- package/templates/commands/analyze.md +39 -3
- package/templates/commands/ask.md +58 -3
- package/templates/commands/bug.md +117 -26
- package/templates/commands/dash.md +95 -158
- package/templates/commands/done.md +130 -148
- package/templates/commands/feature.md +125 -103
- package/templates/commands/git.md +18 -3
- package/templates/commands/idea.md +121 -38
- package/templates/commands/init.md +124 -20
- package/templates/commands/migrate-all.md +63 -28
- package/templates/commands/migrate.md +140 -0
- package/templates/commands/next.md +115 -5
- package/templates/commands/now.md +146 -82
- package/templates/commands/pause.md +89 -74
- package/templates/commands/redo.md +6 -4
- package/templates/commands/resume.md +141 -59
- package/templates/commands/ship.md +103 -231
- package/templates/commands/spec.md +98 -8
- package/templates/commands/suggest.md +22 -2
- package/templates/commands/sync.md +192 -203
- package/templates/commands/undo.md +6 -4
- package/core/data/agents-manager.ts +0 -76
- package/core/data/analysis-manager.ts +0 -83
- package/core/data/base-manager.ts +0 -156
- package/core/data/ideas-manager.ts +0 -81
- package/core/data/outcomes-manager.ts +0 -96
- package/core/data/project-manager.ts +0 -75
- package/core/data/roadmap-manager.ts +0 -118
- package/core/data/shipped-manager.ts +0 -65
- package/core/data/state-manager.ts +0 -214
- package/core/state/index.ts +0 -25
- package/core/state/manager.ts +0 -376
- package/core/state/types.ts +0 -185
- package/core/utils/project-capabilities.ts +0 -156
- package/core/view-generator.ts +0 -536
- package/packages/web/app/project/[id]/stats/loading.tsx +0 -43
- package/packages/web/app/project/[id]/stats/page.tsx +0 -253
- package/templates/agent-assignment.md +0 -72
- package/templates/analysis/project-analysis.md +0 -78
- package/templates/checklists/accessibility.md +0 -33
- package/templates/commands/build.md +0 -17
- package/templates/commands/decision.md +0 -226
- package/templates/commands/fix.md +0 -79
- package/templates/commands/help.md +0 -61
- package/templates/commands/progress.md +0 -14
- package/templates/commands/recap.md +0 -14
- package/templates/commands/roadmap.md +0 -52
- package/templates/commands/status.md +0 -17
- package/templates/commands/task.md +0 -63
- package/templates/commands/work.md +0 -44
- package/templates/commands/workflow.md +0 -12
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ideas Storage
|
|
3
|
+
*
|
|
4
|
+
* Manages ideas via storage/ideas.json
|
|
5
|
+
* Generates context/ideas.md for Claude
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { StorageManager } from './storage-manager'
|
|
9
|
+
import { generateUUID } from '../schemas'
|
|
10
|
+
|
|
11
|
+
export type IdeaStatus = 'pending' | 'converted' | 'archived'
|
|
12
|
+
export type IdeaPriority = 'low' | 'medium' | 'high'
|
|
13
|
+
|
|
14
|
+
export interface Idea {
|
|
15
|
+
id: string
|
|
16
|
+
text: string
|
|
17
|
+
status: IdeaStatus
|
|
18
|
+
priority: IdeaPriority
|
|
19
|
+
tags: string[]
|
|
20
|
+
addedAt: string
|
|
21
|
+
convertedTo?: string // featureId if converted
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface IdeasJson {
|
|
25
|
+
ideas: Idea[]
|
|
26
|
+
lastUpdated: string
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
class IdeasStorage extends StorageManager<IdeasJson> {
|
|
30
|
+
constructor() {
|
|
31
|
+
super('ideas.json')
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
protected getDefault(): IdeasJson {
|
|
35
|
+
return {
|
|
36
|
+
ideas: [],
|
|
37
|
+
lastUpdated: ''
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
protected getMdFilename(): string {
|
|
42
|
+
return 'ideas.md'
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
protected getEventType(action: 'update' | 'create' | 'delete'): string {
|
|
46
|
+
return `ideas.${action}d`
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
protected toMarkdown(data: IdeasJson): string {
|
|
50
|
+
const lines = ['# IDEAS \u{1F4A1}', '']
|
|
51
|
+
|
|
52
|
+
const pending = data.ideas.filter(i => i.status === 'pending')
|
|
53
|
+
const converted = data.ideas.filter(i => i.status === 'converted')
|
|
54
|
+
const archived = data.ideas.filter(i => i.status === 'archived')
|
|
55
|
+
|
|
56
|
+
// Brain Dump (pending)
|
|
57
|
+
lines.push('## Brain Dump')
|
|
58
|
+
if (pending.length > 0) {
|
|
59
|
+
pending.forEach(idea => {
|
|
60
|
+
const date = idea.addedAt.split('T')[0]
|
|
61
|
+
const tags = idea.tags.length > 0 ? ' ' + idea.tags.map(t => `#${t}`).join(' ') : ''
|
|
62
|
+
const priority = idea.priority !== 'medium' ? ` [${idea.priority.toUpperCase()}]` : ''
|
|
63
|
+
lines.push(`- ${idea.text}${priority} _(${date})_${tags}`)
|
|
64
|
+
})
|
|
65
|
+
} else {
|
|
66
|
+
lines.push('_No pending ideas_')
|
|
67
|
+
}
|
|
68
|
+
lines.push('')
|
|
69
|
+
|
|
70
|
+
// Converted
|
|
71
|
+
if (converted.length > 0) {
|
|
72
|
+
lines.push('## Converted')
|
|
73
|
+
converted.forEach(idea => {
|
|
74
|
+
const date = idea.addedAt.split('T')[0]
|
|
75
|
+
const feat = idea.convertedTo ? ` \u2192 ${idea.convertedTo}` : ''
|
|
76
|
+
lines.push(`- \u2713 ${idea.text}${feat} _(${date})_`)
|
|
77
|
+
})
|
|
78
|
+
lines.push('')
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Archived
|
|
82
|
+
if (archived.length > 0) {
|
|
83
|
+
lines.push('## Archived')
|
|
84
|
+
archived.forEach(idea => {
|
|
85
|
+
const date = idea.addedAt.split('T')[0]
|
|
86
|
+
lines.push(`- ${idea.text} _(${date})_`)
|
|
87
|
+
})
|
|
88
|
+
lines.push('')
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return lines.join('\n')
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// =========== Domain Methods ===========
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Get all ideas
|
|
98
|
+
*/
|
|
99
|
+
async getAll(projectId: string): Promise<Idea[]> {
|
|
100
|
+
const data = await this.read(projectId)
|
|
101
|
+
return data.ideas
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Get pending ideas
|
|
106
|
+
*/
|
|
107
|
+
async getPending(projectId: string): Promise<Idea[]> {
|
|
108
|
+
const data = await this.read(projectId)
|
|
109
|
+
return data.ideas.filter(i => i.status === 'pending')
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Add a new idea
|
|
114
|
+
*/
|
|
115
|
+
async addIdea(
|
|
116
|
+
projectId: string,
|
|
117
|
+
text: string,
|
|
118
|
+
options: { tags?: string[]; priority?: IdeaPriority } = {}
|
|
119
|
+
): Promise<Idea> {
|
|
120
|
+
const idea: Idea = {
|
|
121
|
+
id: generateUUID(),
|
|
122
|
+
text,
|
|
123
|
+
status: 'pending',
|
|
124
|
+
priority: options.priority || 'medium',
|
|
125
|
+
tags: options.tags || [],
|
|
126
|
+
addedAt: new Date().toISOString()
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
await this.update(projectId, (data) => ({
|
|
130
|
+
ideas: [idea, ...data.ideas], // Prepend new ideas
|
|
131
|
+
lastUpdated: new Date().toISOString()
|
|
132
|
+
}))
|
|
133
|
+
|
|
134
|
+
// Publish event
|
|
135
|
+
await this.publishEvent(projectId, 'idea.created', {
|
|
136
|
+
ideaId: idea.id,
|
|
137
|
+
text: idea.text,
|
|
138
|
+
priority: idea.priority
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
return idea
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Get idea by ID
|
|
146
|
+
*/
|
|
147
|
+
async getById(projectId: string, id: string): Promise<Idea | undefined> {
|
|
148
|
+
const data = await this.read(projectId)
|
|
149
|
+
return data.ideas.find(i => i.id === id)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Convert idea to feature
|
|
154
|
+
*/
|
|
155
|
+
async convertToFeature(
|
|
156
|
+
projectId: string,
|
|
157
|
+
ideaId: string,
|
|
158
|
+
featureId: string
|
|
159
|
+
): Promise<void> {
|
|
160
|
+
await this.update(projectId, (data) => ({
|
|
161
|
+
ideas: data.ideas.map(i =>
|
|
162
|
+
i.id === ideaId
|
|
163
|
+
? { ...i, status: 'converted' as IdeaStatus, convertedTo: featureId }
|
|
164
|
+
: i
|
|
165
|
+
),
|
|
166
|
+
lastUpdated: new Date().toISOString()
|
|
167
|
+
}))
|
|
168
|
+
|
|
169
|
+
await this.publishEvent(projectId, 'idea.converted', {
|
|
170
|
+
ideaId,
|
|
171
|
+
featureId
|
|
172
|
+
})
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Archive an idea
|
|
177
|
+
*/
|
|
178
|
+
async archive(projectId: string, ideaId: string): Promise<void> {
|
|
179
|
+
await this.update(projectId, (data) => ({
|
|
180
|
+
ideas: data.ideas.map(i =>
|
|
181
|
+
i.id === ideaId ? { ...i, status: 'archived' as IdeaStatus } : i
|
|
182
|
+
),
|
|
183
|
+
lastUpdated: new Date().toISOString()
|
|
184
|
+
}))
|
|
185
|
+
|
|
186
|
+
await this.publishEvent(projectId, 'idea.archived', { ideaId })
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Set priority
|
|
191
|
+
*/
|
|
192
|
+
async setPriority(
|
|
193
|
+
projectId: string,
|
|
194
|
+
ideaId: string,
|
|
195
|
+
priority: IdeaPriority
|
|
196
|
+
): Promise<void> {
|
|
197
|
+
await this.update(projectId, (data) => ({
|
|
198
|
+
ideas: data.ideas.map(i =>
|
|
199
|
+
i.id === ideaId ? { ...i, priority } : i
|
|
200
|
+
),
|
|
201
|
+
lastUpdated: new Date().toISOString()
|
|
202
|
+
}))
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Add tags to an idea
|
|
207
|
+
*/
|
|
208
|
+
async addTags(
|
|
209
|
+
projectId: string,
|
|
210
|
+
ideaId: string,
|
|
211
|
+
tags: string[]
|
|
212
|
+
): Promise<void> {
|
|
213
|
+
await this.update(projectId, (data) => ({
|
|
214
|
+
ideas: data.ideas.map(i =>
|
|
215
|
+
i.id === ideaId
|
|
216
|
+
? { ...i, tags: [...new Set([...i.tags, ...tags])] }
|
|
217
|
+
: i
|
|
218
|
+
),
|
|
219
|
+
lastUpdated: new Date().toISOString()
|
|
220
|
+
}))
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Remove an idea
|
|
225
|
+
*/
|
|
226
|
+
async removeIdea(projectId: string, ideaId: string): Promise<void> {
|
|
227
|
+
await this.update(projectId, (data) => ({
|
|
228
|
+
ideas: data.ideas.filter(i => i.id !== ideaId),
|
|
229
|
+
lastUpdated: new Date().toISOString()
|
|
230
|
+
}))
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Get counts by status
|
|
235
|
+
*/
|
|
236
|
+
async getCounts(projectId: string): Promise<{ pending: number; converted: number; archived: number }> {
|
|
237
|
+
const data = await this.read(projectId)
|
|
238
|
+
return {
|
|
239
|
+
pending: data.ideas.filter(i => i.status === 'pending').length,
|
|
240
|
+
converted: data.ideas.filter(i => i.status === 'converted').length,
|
|
241
|
+
archived: data.ideas.filter(i => i.status === 'archived').length
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Cleanup old archived ideas (keep last 50)
|
|
247
|
+
*/
|
|
248
|
+
async cleanup(projectId: string): Promise<{ removed: number }> {
|
|
249
|
+
const data = await this.read(projectId)
|
|
250
|
+
const archived = data.ideas.filter(i => i.status === 'archived')
|
|
251
|
+
|
|
252
|
+
if (archived.length <= 50) {
|
|
253
|
+
return { removed: 0 }
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Sort by date and keep newest 50
|
|
257
|
+
const sortedArchived = archived.sort(
|
|
258
|
+
(a, b) => new Date(b.addedAt).getTime() - new Date(a.addedAt).getTime()
|
|
259
|
+
)
|
|
260
|
+
const toRemove = new Set(sortedArchived.slice(50).map(i => i.id))
|
|
261
|
+
const removed = toRemove.size
|
|
262
|
+
|
|
263
|
+
await this.update(projectId, (d) => ({
|
|
264
|
+
ideas: d.ideas.filter(i => !toRemove.has(i.id)),
|
|
265
|
+
lastUpdated: new Date().toISOString()
|
|
266
|
+
}))
|
|
267
|
+
|
|
268
|
+
return { removed }
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export const ideasStorage = new IdeasStorage()
|
|
273
|
+
export default ideasStorage
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Storage Layer
|
|
3
|
+
*
|
|
4
|
+
* Two storage patterns:
|
|
5
|
+
*
|
|
6
|
+
* 1. AGGREGATE STORAGE (Write-Through Pattern)
|
|
7
|
+
* For main state - writes JSON + regenerates MD + publishes events
|
|
8
|
+
* - stateStorage: storage/state.json → context/now.md
|
|
9
|
+
* - queueStorage: storage/queue.json → context/next.md
|
|
10
|
+
* - ideasStorage: storage/ideas.json → context/ideas.md
|
|
11
|
+
* - shippedStorage: storage/shipped.json → context/shipped.md
|
|
12
|
+
*
|
|
13
|
+
* 2. GRANULAR STORAGE (OpenCode-style) - Legacy
|
|
14
|
+
* For future per-entity storage
|
|
15
|
+
* - getStorage(projectId): data/{entity}s/{id}.json
|
|
16
|
+
*
|
|
17
|
+
* Structure:
|
|
18
|
+
* ~/.prjct-cli/projects/{projectId}/
|
|
19
|
+
* ├── storage/ # Aggregate JSON (source of truth)
|
|
20
|
+
* │ ├── state.json
|
|
21
|
+
* │ ├── queue.json
|
|
22
|
+
* │ ├── ideas.json
|
|
23
|
+
* │ └── shipped.json
|
|
24
|
+
* ├── context/ # Generated MD (for Claude)
|
|
25
|
+
* │ ├── CLAUDE.md
|
|
26
|
+
* │ ├── now.md
|
|
27
|
+
* │ ├── next.md
|
|
28
|
+
* │ ├── ideas.md
|
|
29
|
+
* │ └── shipped.md
|
|
30
|
+
* ├── data/ # Granular JSON (legacy/future)
|
|
31
|
+
* │ └── ...
|
|
32
|
+
* └── sync/ # Backend sync
|
|
33
|
+
* ├── pending.json
|
|
34
|
+
* └── last-sync.json
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
// ========== AGGREGATE STORAGE (Recommended) ==========
|
|
38
|
+
export { StorageManager } from './storage-manager'
|
|
39
|
+
export { stateStorage } from './state-storage'
|
|
40
|
+
export { queueStorage } from './queue-storage'
|
|
41
|
+
export { ideasStorage, type Idea, type IdeasJson, type IdeaStatus, type IdeaPriority } from './ideas-storage'
|
|
42
|
+
export { shippedStorage, type ShippedFeature, type ShippedJson } from './shipped-storage'
|
|
43
|
+
|
|
44
|
+
// ========== GRANULAR STORAGE (Legacy) ==========
|
|
45
|
+
|
|
46
|
+
import fs from 'fs/promises'
|
|
47
|
+
import path from 'path'
|
|
48
|
+
import os from 'os'
|
|
49
|
+
import { eventBus, inferEventType } from '../events'
|
|
50
|
+
|
|
51
|
+
export interface Storage {
|
|
52
|
+
write<T>(path: string[], data: T): Promise<void>
|
|
53
|
+
read<T>(path: string[]): Promise<T | null>
|
|
54
|
+
list(prefix: string[]): Promise<string[][]>
|
|
55
|
+
delete(path: string[]): Promise<void>
|
|
56
|
+
exists(path: string[]): Promise<boolean>
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
class FileStorage implements Storage {
|
|
60
|
+
private projectId: string
|
|
61
|
+
private basePath: string
|
|
62
|
+
|
|
63
|
+
constructor(projectId: string) {
|
|
64
|
+
this.projectId = projectId
|
|
65
|
+
this.basePath = path.join(os.homedir(), '.prjct-cli/projects', projectId, 'data')
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Convert path array to file path
|
|
70
|
+
* ["task", "abc123"] → data/tasks/abc123.json
|
|
71
|
+
* ["project"] → data/project.json
|
|
72
|
+
*/
|
|
73
|
+
private pathToFile(pathArray: string[]): string {
|
|
74
|
+
if (pathArray.length === 1) {
|
|
75
|
+
return path.join(this.basePath, `${pathArray[0]}.json`)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Pluralize first segment for directory
|
|
79
|
+
const dir = pathArray[0] + 's'
|
|
80
|
+
const rest = pathArray.slice(1)
|
|
81
|
+
const filename = rest.join('/') + '.json'
|
|
82
|
+
|
|
83
|
+
return path.join(this.basePath, dir, filename)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async write<T>(pathArray: string[], data: T): Promise<void> {
|
|
87
|
+
const filePath = this.pathToFile(pathArray)
|
|
88
|
+
|
|
89
|
+
// Ensure directory exists
|
|
90
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true })
|
|
91
|
+
|
|
92
|
+
// Write data
|
|
93
|
+
await fs.writeFile(filePath, JSON.stringify(data, null, 2), 'utf-8')
|
|
94
|
+
|
|
95
|
+
// Publish event for sync
|
|
96
|
+
eventBus.publish({
|
|
97
|
+
type: inferEventType(pathArray, 'write'),
|
|
98
|
+
path: pathArray,
|
|
99
|
+
data,
|
|
100
|
+
timestamp: new Date().toISOString(),
|
|
101
|
+
projectId: this.projectId
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
// Update index if it's a collection item
|
|
105
|
+
if (pathArray.length === 2) {
|
|
106
|
+
await this.updateIndex(pathArray[0], pathArray[1], 'add')
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async read<T>(pathArray: string[]): Promise<T | null> {
|
|
111
|
+
const filePath = this.pathToFile(pathArray)
|
|
112
|
+
|
|
113
|
+
try {
|
|
114
|
+
const content = await fs.readFile(filePath, 'utf-8')
|
|
115
|
+
return JSON.parse(content) as T
|
|
116
|
+
} catch {
|
|
117
|
+
return null
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async list(prefix: string[]): Promise<string[][]> {
|
|
122
|
+
const dir = path.join(this.basePath, prefix[0] + 's')
|
|
123
|
+
|
|
124
|
+
try {
|
|
125
|
+
const files = await fs.readdir(dir)
|
|
126
|
+
return files
|
|
127
|
+
.filter(f => f.endsWith('.json') && f !== 'index.json')
|
|
128
|
+
.map(f => [...prefix, f.replace('.json', '')])
|
|
129
|
+
} catch {
|
|
130
|
+
return []
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async delete(pathArray: string[]): Promise<void> {
|
|
135
|
+
const filePath = this.pathToFile(pathArray)
|
|
136
|
+
|
|
137
|
+
try {
|
|
138
|
+
await fs.unlink(filePath)
|
|
139
|
+
|
|
140
|
+
// Publish event for sync
|
|
141
|
+
eventBus.publish({
|
|
142
|
+
type: inferEventType(pathArray, 'delete'),
|
|
143
|
+
path: pathArray,
|
|
144
|
+
data: null,
|
|
145
|
+
timestamp: new Date().toISOString(),
|
|
146
|
+
projectId: this.projectId
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
// Update index if it's a collection item
|
|
150
|
+
if (pathArray.length === 2) {
|
|
151
|
+
await this.updateIndex(pathArray[0], pathArray[1], 'remove')
|
|
152
|
+
}
|
|
153
|
+
} catch {
|
|
154
|
+
// File doesn't exist, ignore
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async exists(pathArray: string[]): Promise<boolean> {
|
|
159
|
+
const filePath = this.pathToFile(pathArray)
|
|
160
|
+
|
|
161
|
+
try {
|
|
162
|
+
await fs.access(filePath)
|
|
163
|
+
return true
|
|
164
|
+
} catch {
|
|
165
|
+
return false
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Update collection index
|
|
171
|
+
*/
|
|
172
|
+
private async updateIndex(collection: string, id: string, action: 'add' | 'remove'): Promise<void> {
|
|
173
|
+
const indexPath = path.join(this.basePath, collection + 's', 'index.json')
|
|
174
|
+
|
|
175
|
+
let index: { ids: string[]; updatedAt: string } = { ids: [], updatedAt: '' }
|
|
176
|
+
|
|
177
|
+
try {
|
|
178
|
+
const content = await fs.readFile(indexPath, 'utf-8')
|
|
179
|
+
index = JSON.parse(content)
|
|
180
|
+
} catch {
|
|
181
|
+
// Index doesn't exist yet
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (action === 'add' && !index.ids.includes(id)) {
|
|
185
|
+
index.ids.push(id)
|
|
186
|
+
} else if (action === 'remove') {
|
|
187
|
+
index.ids = index.ids.filter(i => i !== id)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
index.updatedAt = new Date().toISOString()
|
|
191
|
+
|
|
192
|
+
await fs.mkdir(path.dirname(indexPath), { recursive: true })
|
|
193
|
+
await fs.writeFile(indexPath, JSON.stringify(index, null, 2), 'utf-8')
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Get storage instance for a project
|
|
199
|
+
*/
|
|
200
|
+
export function getStorage(projectId: string): Storage {
|
|
201
|
+
return new FileStorage(projectId)
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export default { getStorage }
|