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.
- package/CHANGELOG.md +122 -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/setup.md +18 -3
- 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/templates/mcp-config.json +20 -1
- 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,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
|
+
}
|