prjct-cli 0.13.3 → 0.15.1

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 (195) hide show
  1. package/CHANGELOG.md +122 -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/setup.md +18 -3
  160. package/templates/commands/ship.md +103 -231
  161. package/templates/commands/spec.md +98 -8
  162. package/templates/commands/suggest.md +22 -2
  163. package/templates/commands/sync.md +192 -203
  164. package/templates/commands/undo.md +6 -4
  165. package/templates/mcp-config.json +20 -1
  166. package/core/data/agents-manager.ts +0 -76
  167. package/core/data/analysis-manager.ts +0 -83
  168. package/core/data/base-manager.ts +0 -156
  169. package/core/data/ideas-manager.ts +0 -81
  170. package/core/data/outcomes-manager.ts +0 -96
  171. package/core/data/project-manager.ts +0 -75
  172. package/core/data/roadmap-manager.ts +0 -118
  173. package/core/data/shipped-manager.ts +0 -65
  174. package/core/data/state-manager.ts +0 -214
  175. package/core/state/index.ts +0 -25
  176. package/core/state/manager.ts +0 -376
  177. package/core/state/types.ts +0 -185
  178. package/core/utils/project-capabilities.ts +0 -156
  179. package/core/view-generator.ts +0 -536
  180. package/packages/web/app/project/[id]/stats/loading.tsx +0 -43
  181. package/packages/web/app/project/[id]/stats/page.tsx +0 -253
  182. package/templates/agent-assignment.md +0 -72
  183. package/templates/analysis/project-analysis.md +0 -78
  184. package/templates/checklists/accessibility.md +0 -33
  185. package/templates/commands/build.md +0 -17
  186. package/templates/commands/decision.md +0 -226
  187. package/templates/commands/fix.md +0 -79
  188. package/templates/commands/help.md +0 -61
  189. package/templates/commands/progress.md +0 -14
  190. package/templates/commands/recap.md +0 -14
  191. package/templates/commands/roadmap.md +0 -52
  192. package/templates/commands/status.md +0 -17
  193. package/templates/commands/task.md +0 -63
  194. package/templates/commands/work.md +0 -44
  195. package/templates/commands/workflow.md +0 -12
@@ -0,0 +1,750 @@
1
+ /**
2
+ * UUID Migration + Structure Migration
3
+ *
4
+ * Migrates:
5
+ * 1. Project IDs from old formats (hash/timestamp) to standard UUIDs
6
+ * 2. Old MD-First structure to new OpenCode-style JSON storage
7
+ *
8
+ * Old structure:
9
+ * ~/.prjct-cli/projects/{projectId}/
10
+ * ├── CLAUDE.md
11
+ * ├── project.json
12
+ * ├── core/ → now.md, next.md
13
+ * ├── progress/ → shipped.md, sessions/
14
+ * ├── planning/ → ideas.md, roadmap.md
15
+ * ├── analysis/ → repo-summary.md
16
+ * ├── memory/ → context.jsonl
17
+ * └── agents/ → *.md
18
+ *
19
+ * New structure:
20
+ * ~/.prjct-cli/projects/{projectId}/
21
+ * ├── data/ # JSON storage (source of truth)
22
+ * ├── context/ # Generated MD for Claude
23
+ * └── sync/ # Sync state
24
+ */
25
+
26
+ import crypto from 'crypto'
27
+ import fs from 'fs/promises'
28
+ import path from 'path'
29
+ import os from 'os'
30
+ import pathManager from './path-manager'
31
+ import configManager from './config-manager'
32
+ import * as fileHelper from '../utils/file-helper'
33
+ import { generateContext } from '../context/generator'
34
+
35
+ const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
36
+
37
+ /**
38
+ * Check if a string is a valid UUID.
39
+ */
40
+ export function isUUID(id: string): boolean {
41
+ return UUID_REGEX.test(id)
42
+ }
43
+
44
+ /**
45
+ * Migration result.
46
+ */
47
+ export interface MigrationResult {
48
+ success: boolean
49
+ oldId: string
50
+ newId: string
51
+ skipped: boolean
52
+ error?: string
53
+ migrated?: {
54
+ tasks: number
55
+ ideas: number
56
+ features: number
57
+ agents: number
58
+ shipped: number
59
+ }
60
+ }
61
+
62
+ /**
63
+ * Check if project has old structure (needs data migration)
64
+ */
65
+ async function hasOldStructure(globalPath: string): Promise<boolean> {
66
+ const oldDirs = ['core', 'progress', 'planning', 'memory', 'agents', 'analysis']
67
+ for (const dir of oldDirs) {
68
+ try {
69
+ await fs.access(path.join(globalPath, dir))
70
+ return true
71
+ } catch {
72
+ // Continue checking
73
+ }
74
+ }
75
+ // Check for root CLAUDE.md or project.json
76
+ try {
77
+ await fs.access(path.join(globalPath, 'CLAUDE.md'))
78
+ return true
79
+ } catch {
80
+ // Continue
81
+ }
82
+ try {
83
+ await fs.access(path.join(globalPath, 'project.json'))
84
+ // Only old if data/ doesn't exist
85
+ try {
86
+ await fs.access(path.join(globalPath, 'data'))
87
+ return false
88
+ } catch {
89
+ return true
90
+ }
91
+ } catch {
92
+ return false
93
+ }
94
+ }
95
+
96
+ /**
97
+ * Parse now.md to extract current task
98
+ */
99
+ function parseNowMd(content: string): { description: string; startedAt?: string } | null {
100
+ if (!content || content.includes('No current task') || content.includes('_No active task_')) {
101
+ return null
102
+ }
103
+
104
+ // Try to extract task description
105
+ const lines = content.split('\n').filter(l => l.trim() && !l.startsWith('#'))
106
+ if (lines.length === 0) return null
107
+
108
+ // Look for **Task:** format or just take first non-empty line
109
+ const taskMatch = content.match(/\*\*Task:\*\*\s*(.+)/i)
110
+ const startMatch = content.match(/\*\*Started:\*\*\s*(.+)/i)
111
+
112
+ return {
113
+ description: taskMatch ? taskMatch[1].trim() : lines[0].replace(/^[-*]\s*/, '').trim(),
114
+ startedAt: startMatch ? startMatch[1].trim() : undefined
115
+ }
116
+ }
117
+
118
+ /**
119
+ * Parse next.md to extract queue tasks
120
+ */
121
+ function parseNextMd(content: string): { description: string; priority?: string }[] {
122
+ if (!content) return []
123
+
124
+ const tasks: { description: string; priority?: string }[] = []
125
+ const lines = content.split('\n')
126
+
127
+ for (const line of lines) {
128
+ // Match numbered or bulleted items
129
+ const match = line.match(/^[\d]+\.\s*(.+)$/) || line.match(/^[-*]\s*(.+)$/)
130
+ if (match) {
131
+ const text = match[1].trim()
132
+ if (text && !text.startsWith('#') && text !== 'Priority Queue' && text !== '_Empty_') {
133
+ // Check for priority tag [high], [medium], [low]
134
+ const priorityMatch = text.match(/\[(high|medium|low|critical)\]/i)
135
+ tasks.push({
136
+ description: text.replace(/\[(high|medium|low|critical)\]/i, '').trim(),
137
+ priority: priorityMatch ? priorityMatch[1].toLowerCase() : undefined
138
+ })
139
+ }
140
+ }
141
+ }
142
+
143
+ return tasks
144
+ }
145
+
146
+ /**
147
+ * Parse ideas.md to extract ideas
148
+ */
149
+ function parseIdeasMd(content: string): { title: string; status?: string }[] {
150
+ if (!content) return []
151
+
152
+ const ideas: { title: string; status?: string }[] = []
153
+ const lines = content.split('\n')
154
+
155
+ for (const line of lines) {
156
+ // Match bulleted items
157
+ const match = line.match(/^[-*]\s*(.+)$/)
158
+ if (match) {
159
+ const text = match[1].trim()
160
+ if (text && !text.startsWith('#') && text !== 'Brain Dump' && text !== '_None_') {
161
+ ideas.push({ title: text, status: 'pending' })
162
+ }
163
+ }
164
+ }
165
+
166
+ return ideas
167
+ }
168
+
169
+ /**
170
+ * Parse roadmap.md to extract features
171
+ */
172
+ function parseRoadmapMd(content: string): { name: string; status: string; description?: string }[] {
173
+ if (!content) return []
174
+
175
+ const features: { name: string; status: string; description?: string }[] = []
176
+ const lines = content.split('\n')
177
+ let currentStatus = 'planned'
178
+
179
+ for (const line of lines) {
180
+ // Detect status headers
181
+ if (line.includes('In Progress') || line.includes('Active')) {
182
+ currentStatus = 'in_progress'
183
+ } else if (line.includes('Planned') || line.includes('Backlog')) {
184
+ currentStatus = 'planned'
185
+ } else if (line.includes('Completed') || line.includes('Done')) {
186
+ currentStatus = 'completed'
187
+ }
188
+
189
+ // Match feature items
190
+ const match = line.match(/^[-*]\s*\*\*(.+?)\*\*:?\s*(.*)$/) || line.match(/^[-*]\s*(.+)$/)
191
+ if (match) {
192
+ const name = match[1].trim()
193
+ const description = match[2]?.trim()
194
+ if (name && !name.startsWith('#') && name !== '_None_') {
195
+ features.push({ name, status: currentStatus, description })
196
+ }
197
+ }
198
+ }
199
+
200
+ return features
201
+ }
202
+
203
+ /**
204
+ * Parse shipped.md to extract shipped items
205
+ */
206
+ function parseShippedMd(content: string): { name: string; date?: string }[] {
207
+ if (!content) return []
208
+
209
+ const shipped: { name: string; date?: string }[] = []
210
+ const lines = content.split('\n')
211
+
212
+ for (const line of lines) {
213
+ // Match items like "- Feature name (2025-12-01)" or just "- Feature name"
214
+ const match = line.match(/^[-*]\s*(.+?)(?:\s*\((\d{4}-\d{2}-\d{2})\))?$/)
215
+ if (match) {
216
+ const name = match[1].trim()
217
+ const date = match[2]
218
+ if (name && !name.startsWith('#') && name !== '_None_') {
219
+ shipped.push({ name, date })
220
+ }
221
+ }
222
+ }
223
+
224
+ return shipped
225
+ }
226
+
227
+ /**
228
+ * Parse agent MD file to extract agent config
229
+ */
230
+ function parseAgentMd(content: string, filename: string): { name: string; role?: string; domain?: string; expertise?: string[] } {
231
+ const name = filename.replace('.md', '')
232
+
233
+ // Try to extract role from content
234
+ const roleMatch = content.match(/\*\*Role:\*\*\s*(.+)/i) || content.match(/^#\s*(.+)/m)
235
+ const domainMatch = content.match(/\*\*Domain:\*\*\s*(.+)/i)
236
+ const expertiseMatch = content.match(/\*\*Expertise:\*\*\s*(.+)/i)
237
+
238
+ return {
239
+ name,
240
+ role: roleMatch ? roleMatch[1].trim() : undefined,
241
+ domain: domainMatch ? domainMatch[1].trim() : undefined,
242
+ expertise: expertiseMatch ? expertiseMatch[1].split(',').map(e => e.trim()) : undefined
243
+ }
244
+ }
245
+
246
+ /**
247
+ * Read file safely
248
+ */
249
+ async function readFileSafe(filePath: string): Promise<string | null> {
250
+ try {
251
+ return await fs.readFile(filePath, 'utf-8')
252
+ } catch {
253
+ return null
254
+ }
255
+ }
256
+
257
+ /**
258
+ * Generate short ID
259
+ */
260
+ function generateId(): string {
261
+ return crypto.randomUUID().split('-')[0]
262
+ }
263
+
264
+ /**
265
+ * Migrate data from old structure to new structure
266
+ */
267
+ async function migrateData(projectId: string, repoPath?: string): Promise<MigrationResult['migrated']> {
268
+ const globalPath = pathManager.getGlobalProjectPath(projectId)
269
+ const stats = { tasks: 0, ideas: 0, features: 0, agents: 0, shipped: 0 }
270
+ let projectRepoPath = repoPath
271
+
272
+ // Ensure new directories exist
273
+ const dirs = ['data', 'data/tasks', 'data/features', 'data/ideas', 'data/sessions', 'data/shipped', 'data/agents', 'context', 'sync']
274
+ for (const dir of dirs) {
275
+ await fs.mkdir(path.join(globalPath, dir), { recursive: true })
276
+ }
277
+
278
+ // 1. Migrate project.json
279
+ const oldProjectJson = await readFileSafe(path.join(globalPath, 'project.json'))
280
+ if (oldProjectJson) {
281
+ try {
282
+ const oldData = JSON.parse(oldProjectJson)
283
+ projectRepoPath = projectRepoPath || oldData.repoPath
284
+ const newProjectData = {
285
+ id: projectId,
286
+ name: oldData.name || null,
287
+ repoPath: oldData.repoPath || null,
288
+ techStack: oldData.techStack?.languages || oldData.techStack || [],
289
+ version: oldData.version || null,
290
+ createdAt: oldData.createdAt || new Date().toISOString(),
291
+ updatedAt: new Date().toISOString()
292
+ }
293
+ await fs.writeFile(
294
+ path.join(globalPath, 'data/project.json'),
295
+ JSON.stringify(newProjectData, null, 2)
296
+ )
297
+ } catch {
298
+ // Invalid JSON, create default
299
+ }
300
+ }
301
+
302
+ // 2. Migrate now.md → task with status in_progress
303
+ const nowContent = await readFileSafe(path.join(globalPath, 'core/now.md'))
304
+ if (nowContent) {
305
+ const task = parseNowMd(nowContent)
306
+ if (task) {
307
+ const taskId = generateId()
308
+ await fs.writeFile(
309
+ path.join(globalPath, 'data/tasks', `${taskId}.json`),
310
+ JSON.stringify({
311
+ id: taskId,
312
+ description: task.description,
313
+ status: 'in_progress',
314
+ priority: 'high',
315
+ startedAt: task.startedAt || new Date().toISOString(),
316
+ createdAt: new Date().toISOString()
317
+ }, null, 2)
318
+ )
319
+ stats.tasks++
320
+ }
321
+ }
322
+
323
+ // 3. Migrate next.md → tasks with status pending
324
+ const nextContent = await readFileSafe(path.join(globalPath, 'core/next.md'))
325
+ if (nextContent) {
326
+ const tasks = parseNextMd(nextContent)
327
+ for (const task of tasks) {
328
+ const taskId = generateId()
329
+ await fs.writeFile(
330
+ path.join(globalPath, 'data/tasks', `${taskId}.json`),
331
+ JSON.stringify({
332
+ id: taskId,
333
+ description: task.description,
334
+ status: 'pending',
335
+ priority: task.priority || 'medium',
336
+ createdAt: new Date().toISOString()
337
+ }, null, 2)
338
+ )
339
+ stats.tasks++
340
+ }
341
+ }
342
+
343
+ // 4. Migrate ideas.md
344
+ const ideasContent = await readFileSafe(path.join(globalPath, 'planning/ideas.md'))
345
+ if (ideasContent) {
346
+ const ideas = parseIdeasMd(ideasContent)
347
+ for (const idea of ideas) {
348
+ const ideaId = generateId()
349
+ await fs.writeFile(
350
+ path.join(globalPath, 'data/ideas', `${ideaId}.json`),
351
+ JSON.stringify({
352
+ id: ideaId,
353
+ title: idea.title,
354
+ status: idea.status || 'pending',
355
+ createdAt: new Date().toISOString()
356
+ }, null, 2)
357
+ )
358
+ stats.ideas++
359
+ }
360
+ }
361
+
362
+ // 5. Migrate roadmap.md → features
363
+ const roadmapContent = await readFileSafe(path.join(globalPath, 'planning/roadmap.md'))
364
+ if (roadmapContent) {
365
+ const features = parseRoadmapMd(roadmapContent)
366
+ for (const feature of features) {
367
+ const featureId = generateId()
368
+ await fs.writeFile(
369
+ path.join(globalPath, 'data/features', `${featureId}.json`),
370
+ JSON.stringify({
371
+ id: featureId,
372
+ name: feature.name,
373
+ status: feature.status,
374
+ description: feature.description,
375
+ createdAt: new Date().toISOString()
376
+ }, null, 2)
377
+ )
378
+ stats.features++
379
+ }
380
+ }
381
+
382
+ // 6. Migrate shipped.md
383
+ const shippedContent = await readFileSafe(path.join(globalPath, 'progress/shipped.md'))
384
+ if (shippedContent) {
385
+ const shipped = parseShippedMd(shippedContent)
386
+ for (const item of shipped) {
387
+ const shipId = generateId()
388
+ await fs.writeFile(
389
+ path.join(globalPath, 'data/shipped', `${shipId}.json`),
390
+ JSON.stringify({
391
+ id: shipId,
392
+ name: item.name,
393
+ shippedAt: item.date || new Date().toISOString(),
394
+ createdAt: new Date().toISOString()
395
+ }, null, 2)
396
+ )
397
+ stats.shipped++
398
+ }
399
+ }
400
+
401
+ // 7. Migrate agents/*.md
402
+ try {
403
+ const agentsDir = path.join(globalPath, 'agents')
404
+ const agentFiles = await fs.readdir(agentsDir)
405
+ for (const file of agentFiles) {
406
+ if (file.endsWith('.md')) {
407
+ const content = await readFileSafe(path.join(agentsDir, file))
408
+ if (content) {
409
+ const agent = parseAgentMd(content, file)
410
+ await fs.writeFile(
411
+ path.join(globalPath, 'data/agents', `${agent.name}.json`),
412
+ JSON.stringify({
413
+ name: agent.name,
414
+ role: agent.role,
415
+ domain: agent.domain,
416
+ expertise: agent.expertise,
417
+ createdAt: new Date().toISOString()
418
+ }, null, 2)
419
+ )
420
+ stats.agents++
421
+ }
422
+ }
423
+ }
424
+ } catch {
425
+ // agents dir doesn't exist
426
+ }
427
+
428
+ // 8. Create indexes
429
+ const taskFiles = await fs.readdir(path.join(globalPath, 'data/tasks')).catch(() => [])
430
+ const taskIds = taskFiles.filter(f => f.endsWith('.json') && f !== 'index.json').map(f => f.replace('.json', ''))
431
+ await fs.writeFile(
432
+ path.join(globalPath, 'data/tasks/index.json'),
433
+ JSON.stringify({ ids: taskIds, updatedAt: new Date().toISOString() }, null, 2)
434
+ )
435
+
436
+ const ideaFiles = await fs.readdir(path.join(globalPath, 'data/ideas')).catch(() => [])
437
+ const ideaIds = ideaFiles.filter(f => f.endsWith('.json') && f !== 'index.json').map(f => f.replace('.json', ''))
438
+ await fs.writeFile(
439
+ path.join(globalPath, 'data/ideas/index.json'),
440
+ JSON.stringify({ ids: ideaIds, updatedAt: new Date().toISOString() }, null, 2)
441
+ )
442
+
443
+ const featureFiles = await fs.readdir(path.join(globalPath, 'data/features')).catch(() => [])
444
+ const featureIds = featureFiles.filter(f => f.endsWith('.json') && f !== 'index.json').map(f => f.replace('.json', ''))
445
+ await fs.writeFile(
446
+ path.join(globalPath, 'data/features/index.json'),
447
+ JSON.stringify({ ids: featureIds, updatedAt: new Date().toISOString() }, null, 2)
448
+ )
449
+
450
+ // 9. Create sync files
451
+ await fs.writeFile(path.join(globalPath, 'sync/pending.json'), '[]')
452
+ await fs.writeFile(
453
+ path.join(globalPath, 'sync/last-sync.json'),
454
+ JSON.stringify({ timestamp: new Date().toISOString(), success: true }, null, 2)
455
+ )
456
+ await fs.writeFile(path.join(globalPath, 'sync/conflict-log.json'), '[]')
457
+
458
+ // 10. Generate context from repo (REAL DATA, not placeholders)
459
+ // NOTE: Agents are NOT generated here - that's AGENTIC (Claude decides in /p:sync)
460
+ if (projectRepoPath) {
461
+ try {
462
+ await generateContext(projectId, projectRepoPath)
463
+ } catch (err) {
464
+ // If context generation fails, create placeholders
465
+ console.error('Context generation failed:', (err as Error).message)
466
+ await fs.writeFile(path.join(globalPath, 'context/CLAUDE.md'), '# Project Context\n\n_Run /p:sync to generate._\n')
467
+ await fs.writeFile(path.join(globalPath, 'context/now.md'), '# NOW\n\n_No active task._\n')
468
+ await fs.writeFile(path.join(globalPath, 'context/queue.md'), '# QUEUE\n\n_Empty queue._\n')
469
+ await fs.writeFile(path.join(globalPath, 'context/summary.md'), '# SUMMARY\n\n_Run /p:sync to generate._\n')
470
+ }
471
+ } else {
472
+ // No repoPath, create placeholders
473
+ await fs.writeFile(path.join(globalPath, 'context/CLAUDE.md'), '# Project Context\n\n_Run /p:sync to generate._\n')
474
+ await fs.writeFile(path.join(globalPath, 'context/now.md'), '# NOW\n\n_No active task._\n')
475
+ await fs.writeFile(path.join(globalPath, 'context/queue.md'), '# QUEUE\n\n_Empty queue._\n')
476
+ await fs.writeFile(path.join(globalPath, 'context/summary.md'), '# SUMMARY\n\n_Run /p:sync to generate._\n')
477
+ }
478
+
479
+ return stats
480
+ }
481
+
482
+ /**
483
+ * Move old directories to .trash
484
+ */
485
+ async function moveToTrash(projectId: string): Promise<void> {
486
+ const globalPath = pathManager.getGlobalProjectPath(projectId)
487
+ const trashPath = path.join(globalPath, '.trash')
488
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-')
489
+
490
+ // Create trash directory
491
+ await fs.mkdir(path.join(trashPath, timestamp), { recursive: true })
492
+
493
+ // Move old items to trash
494
+ const oldItems = ['core', 'progress', 'planning', 'analysis', 'memory', 'agents', 'sessions', 'state', 'CLAUDE.md', 'project.json']
495
+
496
+ for (const item of oldItems) {
497
+ const oldPath = path.join(globalPath, item)
498
+ const newPath = path.join(trashPath, timestamp, item)
499
+
500
+ try {
501
+ await fs.access(oldPath)
502
+ await fs.rename(oldPath, newPath)
503
+ } catch {
504
+ // Doesn't exist, skip
505
+ }
506
+ }
507
+ }
508
+
509
+ /**
510
+ * Migrate a project's ID to UUID format.
511
+ */
512
+ export async function migrateProjectToUUID(projectPath: string): Promise<MigrationResult> {
513
+ const config = await configManager.readConfig(projectPath)
514
+ if (!config) {
515
+ return {
516
+ success: false,
517
+ oldId: '',
518
+ newId: '',
519
+ skipped: false,
520
+ error: 'Project not initialized'
521
+ }
522
+ }
523
+
524
+ const oldId = config.projectId
525
+
526
+ // Already UUID - skip UUID migration but check structure
527
+ if (isUUID(oldId)) {
528
+ return { success: true, oldId, newId: oldId, skipped: true }
529
+ }
530
+
531
+ const newId = crypto.randomUUID()
532
+
533
+ try {
534
+ // Rename global folder
535
+ const oldPath = pathManager.getGlobalProjectPath(oldId)
536
+ const newPath = pathManager.getGlobalProjectPath(newId)
537
+
538
+ try {
539
+ await fs.access(oldPath)
540
+ } catch {
541
+ return {
542
+ success: false,
543
+ oldId,
544
+ newId,
545
+ skipped: false,
546
+ error: `Global folder not found: ${oldPath}`
547
+ }
548
+ }
549
+
550
+ try {
551
+ await fs.access(newPath)
552
+ return {
553
+ success: false,
554
+ oldId,
555
+ newId,
556
+ skipped: false,
557
+ error: `Target folder already exists: ${newPath}`
558
+ }
559
+ } catch {
560
+ // Good - new path doesn't exist
561
+ }
562
+
563
+ await fs.rename(oldPath, newPath)
564
+
565
+ // Update local config
566
+ config.projectId = newId
567
+ config.dataPath = pathManager.getDisplayPath(newPath)
568
+ await configManager.writeConfig(projectPath, config)
569
+
570
+ // Update global project.json if exists
571
+ const projectJsonPath = path.join(newPath, 'project.json')
572
+ try {
573
+ const content = await fs.readFile(projectJsonPath, 'utf-8')
574
+ const updated = content.replace(new RegExp(oldId, 'g'), newId)
575
+ await fs.writeFile(projectJsonPath, updated)
576
+ } catch {
577
+ // project.json may not exist
578
+ }
579
+
580
+ return { success: true, oldId, newId, skipped: false }
581
+ } catch (error) {
582
+ return {
583
+ success: false,
584
+ oldId,
585
+ newId,
586
+ skipped: false,
587
+ error: (error as Error).message
588
+ }
589
+ }
590
+ }
591
+
592
+ /**
593
+ * Check if a project needs UUID migration.
594
+ */
595
+ export async function needsUUIDMigration(projectPath: string): Promise<boolean> {
596
+ const config = await configManager.readConfig(projectPath)
597
+ if (!config) return false
598
+ return !isUUID(config.projectId)
599
+ }
600
+
601
+ /**
602
+ * Check if a project needs structure migration.
603
+ */
604
+ export async function needsStructureMigration(projectId: string): Promise<boolean> {
605
+ const globalPath = pathManager.getGlobalProjectPath(projectId)
606
+ return await hasOldStructure(globalPath)
607
+ }
608
+
609
+ /**
610
+ * Ensure new structure exists (without migrating data).
611
+ * Creates directories and default files if missing.
612
+ */
613
+ export async function ensureCompleteStructure(projectId: string): Promise<void> {
614
+ const globalPath = pathManager.getGlobalProjectPath(projectId)
615
+
616
+ // Create directories
617
+ const dirs = ['data', 'data/tasks', 'data/features', 'data/ideas', 'data/sessions', 'data/shipped', 'data/agents', 'context', 'sync']
618
+ for (const dir of dirs) {
619
+ await fs.mkdir(path.join(globalPath, dir), { recursive: true })
620
+ }
621
+
622
+ // Create default files only if they don't exist
623
+ const defaults: Record<string, string> = {
624
+ 'data/project.json': JSON.stringify({
625
+ id: projectId,
626
+ name: null,
627
+ repoPath: null,
628
+ techStack: [],
629
+ version: null,
630
+ createdAt: new Date().toISOString(),
631
+ updatedAt: new Date().toISOString()
632
+ }, null, 2),
633
+ 'data/tasks/index.json': JSON.stringify({ ids: [], updatedAt: new Date().toISOString() }, null, 2),
634
+ 'data/features/index.json': JSON.stringify({ ids: [], updatedAt: new Date().toISOString() }, null, 2),
635
+ 'data/ideas/index.json': JSON.stringify({ ids: [], updatedAt: new Date().toISOString() }, null, 2),
636
+ 'sync/pending.json': '[]',
637
+ 'sync/last-sync.json': JSON.stringify({ timestamp: null, success: false }, null, 2),
638
+ 'sync/conflict-log.json': '[]',
639
+ 'context/CLAUDE.md': '# Project Context\n\n_Run /p:sync to generate._\n',
640
+ 'context/now.md': '# NOW\n\n_No active task._\n',
641
+ 'context/queue.md': '# QUEUE\n\n_Empty queue._\n',
642
+ 'context/summary.md': '# SUMMARY\n\n_Run /p:sync to generate._\n'
643
+ }
644
+
645
+ for (const [filePath, content] of Object.entries(defaults)) {
646
+ const fullPath = path.join(globalPath, filePath)
647
+ try {
648
+ await fs.access(fullPath)
649
+ } catch {
650
+ await fs.writeFile(fullPath, content)
651
+ }
652
+ }
653
+ }
654
+
655
+ /**
656
+ * Full migration: UUID + data migration + move to trash
657
+ *
658
+ * 1. UUID migration (if needed)
659
+ * 2. Data migration from old MD files to new JSON structure
660
+ * 3. Move old directories to .trash/
661
+ */
662
+ export async function fullMigration(projectPath: string): Promise<MigrationResult> {
663
+ // 1. UUID Migration
664
+ const result = await migrateProjectToUUID(projectPath)
665
+
666
+ if (!result.success) {
667
+ return result
668
+ }
669
+
670
+ const projectId = result.newId
671
+
672
+ // 2. Check if needs structure migration
673
+ const needsMigration = await needsStructureMigration(projectId)
674
+
675
+ if (needsMigration) {
676
+ // 3. Migrate data from old to new structure
677
+ const migrated = await migrateData(projectId)
678
+ result.migrated = migrated
679
+
680
+ // 4. Move old directories to .trash
681
+ await moveToTrash(projectId)
682
+ } else {
683
+ // Just ensure structure exists
684
+ await ensureCompleteStructure(projectId)
685
+ }
686
+
687
+ return result
688
+ }
689
+
690
+ /**
691
+ * Migrate all projects in ~/.prjct-cli/projects/
692
+ */
693
+ export async function migrateAllProjects(): Promise<{ success: number; failed: number; skipped: number }> {
694
+ const projectsDir = path.join(os.homedir(), '.prjct-cli/projects')
695
+ const stats = { success: 0, failed: 0, skipped: 0 }
696
+
697
+ try {
698
+ const entries = await fs.readdir(projectsDir, { withFileTypes: true })
699
+
700
+ for (const entry of entries) {
701
+ if (!entry.isDirectory() || entry.name.startsWith('.')) continue
702
+
703
+ const projectId = entry.name
704
+ const globalPath = path.join(projectsDir, projectId)
705
+
706
+ // Check if needs migration
707
+ const needsMigration = await hasOldStructure(globalPath)
708
+
709
+ if (!needsMigration) {
710
+ stats.skipped++
711
+ continue
712
+ }
713
+
714
+ try {
715
+ // Get repoPath from old project.json before migration
716
+ let repoPath: string | undefined
717
+ try {
718
+ const oldProjectJson = await fs.readFile(path.join(globalPath, 'project.json'), 'utf-8')
719
+ const oldData = JSON.parse(oldProjectJson)
720
+ repoPath = oldData.repoPath
721
+ } catch {
722
+ // No project.json or invalid
723
+ }
724
+
725
+ // Migrate data (with repoPath for context generation)
726
+ await migrateData(projectId, repoPath)
727
+ // Move to trash
728
+ await moveToTrash(projectId)
729
+ stats.success++
730
+ } catch (error) {
731
+ console.error(`Failed to migrate ${projectId}:`, (error as Error).message)
732
+ stats.failed++
733
+ }
734
+ }
735
+ } catch {
736
+ // projects dir doesn't exist
737
+ }
738
+
739
+ return stats
740
+ }
741
+
742
+ export default {
743
+ isUUID,
744
+ migrateProjectToUUID,
745
+ needsUUIDMigration,
746
+ needsStructureMigration,
747
+ ensureCompleteStructure,
748
+ fullMigration,
749
+ migrateAllProjects
750
+ }