prjct-cli 0.15.1 → 0.18.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 +35 -0
- package/bin/dev.js +0 -1
- package/bin/serve.js +19 -20
- package/core/__tests__/agentic/memory-system.test.ts +2 -1
- package/core/__tests__/agentic/plan-mode.test.ts +2 -1
- package/core/agentic/agent-router.ts +79 -14
- package/core/agentic/command-executor/command-executor.ts +2 -74
- package/core/agentic/services.ts +0 -48
- package/core/agentic/template-loader.ts +35 -1
- package/core/command-registry/setup-commands.ts +15 -0
- package/core/commands/base.ts +96 -77
- package/core/commands/planning.ts +13 -2
- package/core/commands/setup.ts +3 -85
- package/core/domain/agent-generator.ts +9 -17
- package/core/errors.ts +209 -0
- package/core/infrastructure/config-manager.ts +22 -5
- package/core/infrastructure/path-manager.ts +23 -1
- package/core/infrastructure/setup.ts +5 -50
- package/core/storage/ideas-storage.ts +4 -0
- package/core/storage/queue-storage.ts +4 -0
- package/core/storage/shipped-storage.ts +4 -0
- package/core/storage/state-storage.ts +4 -0
- package/core/storage/storage-manager.ts +52 -13
- package/core/sync/auth-config.ts +145 -0
- package/core/sync/index.ts +30 -0
- package/core/sync/oauth-handler.ts +148 -0
- package/core/sync/sync-client.ts +252 -0
- package/core/sync/sync-manager.ts +358 -0
- package/core/utils/logger.ts +19 -12
- package/package.json +2 -4
- package/templates/agentic/subagent-generation.md +109 -0
- package/templates/commands/auth.md +234 -0
- package/templates/commands/sync.md +129 -13
- package/templates/subagents/domain/backend.md +105 -0
- package/templates/subagents/domain/database.md +118 -0
- package/templates/subagents/domain/devops.md +148 -0
- package/templates/subagents/domain/frontend.md +99 -0
- package/templates/subagents/domain/testing.md +169 -0
- package/templates/subagents/workflow/prjct-planner.md +158 -0
- package/templates/subagents/workflow/prjct-shipper.md +179 -0
- package/templates/subagents/workflow/prjct-workflow.md +98 -0
- package/bin/generate-views.js +0 -209
- package/bin/migrate-to-json.js +0 -742
- package/core/agentic/context-filter.ts +0 -365
- package/core/agentic/parallel-tools.ts +0 -165
- package/core/agentic/response-templates.ts +0 -164
- package/core/agentic/semantic-compression.ts +0 -273
- package/core/agentic/think-blocks.ts +0 -202
- package/core/agentic/validation-rules.ts +0 -313
- package/core/domain/agent-matcher.ts +0 -130
- package/core/domain/agent-validator.ts +0 -250
- package/core/domain/architect-session.ts +0 -315
- package/core/domain/product-standards.ts +0 -106
- package/core/domain/smart-cache.ts +0 -167
- package/core/domain/task-analyzer.ts +0 -296
- package/core/infrastructure/legacy-installer-detector/cleanup.ts +0 -216
- package/core/infrastructure/legacy-installer-detector/detection.ts +0 -95
- package/core/infrastructure/legacy-installer-detector/index.ts +0 -171
- package/core/infrastructure/legacy-installer-detector/migration.ts +0 -87
- package/core/infrastructure/legacy-installer-detector/types.ts +0 -42
- package/core/infrastructure/legacy-installer-detector.ts +0 -7
- package/core/infrastructure/migrator/file-operations.ts +0 -125
- package/core/infrastructure/migrator/index.ts +0 -288
- package/core/infrastructure/migrator/project-scanner.ts +0 -90
- package/core/infrastructure/migrator/reports.ts +0 -117
- package/core/infrastructure/migrator/types.ts +0 -124
- package/core/infrastructure/migrator/validation.ts +0 -94
- package/core/infrastructure/migrator/version-migration.ts +0 -117
- package/core/infrastructure/migrator.ts +0 -10
- package/core/infrastructure/uuid-migration.ts +0 -750
- package/templates/commands/migrate-all.md +0 -96
- package/templates/commands/migrate.md +0 -140
package/bin/migrate-to-json.js
DELETED
|
@@ -1,742 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bun
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* prjct migrate-to-json - Complete migration to JSON-first architecture
|
|
5
|
-
*
|
|
6
|
-
* Migrates ALL data to unified JSON structure in data/ directory.
|
|
7
|
-
* Prioritizes MD files (richer data), complements with existing JSON.
|
|
8
|
-
* Deletes legacy files after successful migration.
|
|
9
|
-
*
|
|
10
|
-
* Usage:
|
|
11
|
-
* prjct migrate-to-json --project=<projectId> Migrate specific project
|
|
12
|
-
* prjct migrate-to-json --all Migrate all projects
|
|
13
|
-
* prjct migrate-to-json --dry-run Preview changes
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
const path = require('path')
|
|
17
|
-
const fs = require('fs/promises')
|
|
18
|
-
const os = require('os')
|
|
19
|
-
|
|
20
|
-
// Colors
|
|
21
|
-
const c = {
|
|
22
|
-
reset: '\x1b[0m',
|
|
23
|
-
bold: '\x1b[1m',
|
|
24
|
-
dim: '\x1b[2m',
|
|
25
|
-
cyan: '\x1b[36m',
|
|
26
|
-
green: '\x1b[32m',
|
|
27
|
-
yellow: '\x1b[33m',
|
|
28
|
-
red: '\x1b[31m',
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
const GLOBAL_STORAGE = path.join(os.homedir(), '.prjct-cli', 'projects')
|
|
32
|
-
|
|
33
|
-
// ============================================
|
|
34
|
-
// HELPERS
|
|
35
|
-
// ============================================
|
|
36
|
-
|
|
37
|
-
function generateId(prefix = '') {
|
|
38
|
-
const chars = 'abcdefghijklmnopqrstuvwxyz0123456789'
|
|
39
|
-
let id = ''
|
|
40
|
-
for (let i = 0; i < 8; i++) {
|
|
41
|
-
id += chars[Math.floor(Math.random() * chars.length)]
|
|
42
|
-
}
|
|
43
|
-
return prefix ? `${prefix}_${id}` : id
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
async function readFile(filePath) {
|
|
47
|
-
try {
|
|
48
|
-
return await fs.readFile(filePath, 'utf-8')
|
|
49
|
-
} catch {
|
|
50
|
-
return null
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
async function readJson(filePath) {
|
|
55
|
-
try {
|
|
56
|
-
const content = await fs.readFile(filePath, 'utf-8')
|
|
57
|
-
return JSON.parse(content)
|
|
58
|
-
} catch {
|
|
59
|
-
return null
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
async function writeJson(filePath, data) {
|
|
64
|
-
const dir = path.dirname(filePath)
|
|
65
|
-
await fs.mkdir(dir, { recursive: true })
|
|
66
|
-
await fs.writeFile(filePath, JSON.stringify(data, null, 2), 'utf-8')
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
async function deleteFile(filePath) {
|
|
70
|
-
try {
|
|
71
|
-
await fs.unlink(filePath)
|
|
72
|
-
return true
|
|
73
|
-
} catch {
|
|
74
|
-
return false
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
async function deleteDirIfEmpty(dirPath) {
|
|
79
|
-
try {
|
|
80
|
-
const files = await fs.readdir(dirPath)
|
|
81
|
-
if (files.length === 0) {
|
|
82
|
-
await fs.rmdir(dirPath)
|
|
83
|
-
return true
|
|
84
|
-
}
|
|
85
|
-
} catch {}
|
|
86
|
-
return false
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function normalizeStatus(s) {
|
|
90
|
-
if (!s) return 'skipped'
|
|
91
|
-
const lower = s.toLowerCase()
|
|
92
|
-
if (lower === 'pass' || lower === 'passed' || lower === 'ok') return 'pass'
|
|
93
|
-
if (lower === 'fail' || lower === 'failed' || lower === 'error') return 'fail'
|
|
94
|
-
if (lower === 'warn' || lower === 'warning' || lower === 'warnings') return 'warning'
|
|
95
|
-
return 'skipped'
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
// ============================================
|
|
99
|
-
// PARSERS - Extract data from MD and JSON
|
|
100
|
-
// ============================================
|
|
101
|
-
|
|
102
|
-
/**
|
|
103
|
-
* Parse state from now.md + now.json + sessions/current.json
|
|
104
|
-
*/
|
|
105
|
-
async function parseState(projectPath) {
|
|
106
|
-
const nowMd = await readFile(path.join(projectPath, 'core', 'now.md'))
|
|
107
|
-
const nowJson = await readJson(path.join(projectPath, 'core', 'now.json'))
|
|
108
|
-
const sessionJson = await readJson(path.join(projectPath, 'sessions', 'current.json'))
|
|
109
|
-
|
|
110
|
-
const state = {
|
|
111
|
-
currentTask: null,
|
|
112
|
-
lastUpdated: new Date().toISOString(),
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
// No task
|
|
116
|
-
if (!nowMd || nowMd.includes('No current task') || nowMd.trim() === '') {
|
|
117
|
-
return state
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
// Extract from MD first
|
|
121
|
-
const descMatch = nowMd.match(/\*\*(.+?)\*\*/)
|
|
122
|
-
const description = descMatch ? descMatch[1] : (nowJson?.task || 'Unknown task')
|
|
123
|
-
|
|
124
|
-
const startedMatch = nowMd.match(/Started:\s*(\d{4}-\d{2}-\d{2}T[\d:.]+Z?)/)
|
|
125
|
-
const startedAt = startedMatch ? startedMatch[1] : (nowJson?.startedAt || sessionJson?.startedAt || new Date().toISOString())
|
|
126
|
-
|
|
127
|
-
const sessionMatch = nowMd.match(/Session:\s*(sess_\w+)/)
|
|
128
|
-
const sessionId = sessionMatch ? sessionMatch[1] : (nowJson?.sessionId || sessionJson?.id || `sess_${generateId()}`)
|
|
129
|
-
|
|
130
|
-
const featureMatch = nowMd.match(/Feature:\s*(feat_\w+)/)
|
|
131
|
-
const featureId = featureMatch ? featureMatch[1] : undefined
|
|
132
|
-
|
|
133
|
-
const agentMatch = nowMd.match(/Agent:\s*(\w+)/)
|
|
134
|
-
const agent = agentMatch ? agentMatch[1] : undefined
|
|
135
|
-
|
|
136
|
-
state.currentTask = {
|
|
137
|
-
id: `task_${generateId()}`,
|
|
138
|
-
description,
|
|
139
|
-
startedAt,
|
|
140
|
-
sessionId,
|
|
141
|
-
...(featureId && { featureId }),
|
|
142
|
-
...(agent && { agent }),
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
// Add session metrics if available
|
|
146
|
-
if (sessionJson?.metrics) {
|
|
147
|
-
state.sessionMetrics = sessionJson.metrics
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
return state
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
/**
|
|
154
|
-
* Parse queue from next.md + next.json
|
|
155
|
-
*/
|
|
156
|
-
async function parseQueue(projectPath) {
|
|
157
|
-
const nextMd = await readFile(path.join(projectPath, 'core', 'next.md'))
|
|
158
|
-
const nextJson = await readJson(path.join(projectPath, 'core', 'next.json'))
|
|
159
|
-
|
|
160
|
-
const queue = {
|
|
161
|
-
tasks: [],
|
|
162
|
-
lastUpdated: new Date().toISOString(),
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
if (!nextMd) return queue
|
|
166
|
-
|
|
167
|
-
// Parse from MD (has more context)
|
|
168
|
-
const taskRegex = /^\s*[-*]?\s*(\d+\.\s*)?\[([x ])\]\s*(.+?)(?:\s*\(from:\s*(.+?)\))?$/gm
|
|
169
|
-
let match
|
|
170
|
-
|
|
171
|
-
while ((match = taskRegex.exec(nextMd)) !== null) {
|
|
172
|
-
const completed = match[2].toLowerCase() === 'x'
|
|
173
|
-
let description = match[3].trim()
|
|
174
|
-
const featureContext = match[4]?.trim()
|
|
175
|
-
|
|
176
|
-
// Determine priority
|
|
177
|
-
let priority = 'medium'
|
|
178
|
-
const lineStart = nextMd.lastIndexOf('\n', match.index)
|
|
179
|
-
const sectionBefore = nextMd.substring(Math.max(0, lineStart - 300), match.index)
|
|
180
|
-
|
|
181
|
-
if (sectionBefore.includes('🔴') || sectionBefore.toLowerCase().includes('critical')) priority = 'critical'
|
|
182
|
-
else if (sectionBefore.includes('🟠') || sectionBefore.toLowerCase().includes('high') || description.includes('[HIGH]')) priority = 'high'
|
|
183
|
-
else if (sectionBefore.includes('🟢') || sectionBefore.toLowerCase().includes('low') || description.includes('[LOW]')) priority = 'low'
|
|
184
|
-
|
|
185
|
-
// Clean up description
|
|
186
|
-
description = description.replace(/\[(?:HIGH|MEDIUM|LOW)\]/g, '').replace(/[🐛✅]/g, '').trim()
|
|
187
|
-
|
|
188
|
-
if (description) {
|
|
189
|
-
queue.tasks.push({
|
|
190
|
-
id: `task_${generateId()}`,
|
|
191
|
-
description,
|
|
192
|
-
priority,
|
|
193
|
-
completed,
|
|
194
|
-
...(completed && { completedAt: new Date().toISOString() }),
|
|
195
|
-
createdAt: new Date().toISOString(),
|
|
196
|
-
...(featureContext && { featureId: featureContext }),
|
|
197
|
-
})
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
// Add tasks from JSON that might not be in MD
|
|
202
|
-
if (nextJson?.activeTasks) {
|
|
203
|
-
for (const task of nextJson.activeTasks) {
|
|
204
|
-
const exists = queue.tasks.some(t =>
|
|
205
|
-
t.description.toLowerCase().includes(task.task?.toLowerCase()?.substring(0, 20) || '')
|
|
206
|
-
)
|
|
207
|
-
if (!exists && task.task) {
|
|
208
|
-
queue.tasks.push({
|
|
209
|
-
id: `task_${generateId()}`,
|
|
210
|
-
description: task.task,
|
|
211
|
-
priority: 'medium',
|
|
212
|
-
completed: task.completed || false,
|
|
213
|
-
createdAt: new Date().toISOString(),
|
|
214
|
-
...(task.source && { featureId: task.source }),
|
|
215
|
-
})
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
return queue
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
/**
|
|
224
|
-
* Parse ideas from ideas.md + ideas.json
|
|
225
|
-
*/
|
|
226
|
-
async function parseIdeas(projectPath) {
|
|
227
|
-
const ideasMd = await readFile(path.join(projectPath, 'planning', 'ideas.md'))
|
|
228
|
-
const ideasJson = await readJson(path.join(projectPath, 'planning', 'ideas.json'))
|
|
229
|
-
|
|
230
|
-
const ideas = {
|
|
231
|
-
ideas: [],
|
|
232
|
-
lastUpdated: new Date().toISOString(),
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
// Parse from MD
|
|
236
|
-
if (ideasMd) {
|
|
237
|
-
const sections = ideasMd.split(/(?=^##\s+\d{4}|^###\s+)/m)
|
|
238
|
-
let currentDate = new Date().toISOString().split('T')[0]
|
|
239
|
-
|
|
240
|
-
for (const section of sections) {
|
|
241
|
-
const dateMatch = section.match(/^##\s+(\d{4}-\d{2}-\d{2})/)
|
|
242
|
-
if (dateMatch) {
|
|
243
|
-
currentDate = dateMatch[1]
|
|
244
|
-
continue
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
const ideaMatch = section.match(/^###\s+(.+?)(?:\s+-\s+COMPLETED)?$/m)
|
|
248
|
-
if (!ideaMatch) continue
|
|
249
|
-
|
|
250
|
-
const title = ideaMatch[1].trim()
|
|
251
|
-
if (section.toLowerCase().includes('archived') && !section.includes('**Status**')) continue
|
|
252
|
-
|
|
253
|
-
const statusMatch = section.match(/\*\*Status\*\*:\s*(\w+)/i)
|
|
254
|
-
let status = 'pending'
|
|
255
|
-
if (statusMatch) {
|
|
256
|
-
const raw = statusMatch[1].toLowerCase()
|
|
257
|
-
if (raw === 'completed' || raw === 'done' || section.includes('COMPLETED')) status = 'converted'
|
|
258
|
-
else if (raw === 'archived') status = 'archived'
|
|
259
|
-
else if (raw === 'reviewing') status = 'reviewing'
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
const descMatch = section.match(/\*\*Description\*\*:\s*(.+?)(?:\n\n|\*\*)/s)
|
|
263
|
-
const details = descMatch ? descMatch[1].trim() : undefined
|
|
264
|
-
|
|
265
|
-
const tags = []
|
|
266
|
-
if (section.toLowerCase().includes('ux')) tags.push('ux')
|
|
267
|
-
if (section.toLowerCase().includes('api')) tags.push('api')
|
|
268
|
-
if (section.toLowerCase().includes('performance')) tags.push('performance')
|
|
269
|
-
|
|
270
|
-
ideas.ideas.push({
|
|
271
|
-
id: `idea_${generateId()}`,
|
|
272
|
-
text: title,
|
|
273
|
-
...(details && { details }),
|
|
274
|
-
priority: section.includes('HIGH') ? 'high' : section.includes('LOW') ? 'low' : 'medium',
|
|
275
|
-
status,
|
|
276
|
-
tags,
|
|
277
|
-
createdAt: `${currentDate}T00:00:00.000Z`,
|
|
278
|
-
})
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
// Merge from JSON
|
|
283
|
-
if (ideasJson?.ideas) {
|
|
284
|
-
for (const idea of ideasJson.ideas) {
|
|
285
|
-
const exists = ideas.ideas.some(i =>
|
|
286
|
-
i.text.toLowerCase().includes(idea.text?.toLowerCase()?.substring(0, 20) || '')
|
|
287
|
-
)
|
|
288
|
-
if (!exists && idea.text) {
|
|
289
|
-
ideas.ideas.push({
|
|
290
|
-
id: `idea_${generateId()}`,
|
|
291
|
-
text: idea.text,
|
|
292
|
-
priority: idea.priority || 'medium',
|
|
293
|
-
status: 'pending',
|
|
294
|
-
tags: idea.tags || [],
|
|
295
|
-
createdAt: idea.addedAt || new Date().toISOString(),
|
|
296
|
-
})
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
return ideas
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
/**
|
|
305
|
-
* Parse roadmap from roadmap.md + roadmap.json
|
|
306
|
-
*/
|
|
307
|
-
async function parseRoadmap(projectPath) {
|
|
308
|
-
const roadmapMd = await readFile(path.join(projectPath, 'planning', 'roadmap.md'))
|
|
309
|
-
const roadmapJson = await readJson(path.join(projectPath, 'planning', 'roadmap.json'))
|
|
310
|
-
|
|
311
|
-
const roadmap = {
|
|
312
|
-
features: [],
|
|
313
|
-
backlog: [],
|
|
314
|
-
lastUpdated: new Date().toISOString(),
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
if (roadmapMd) {
|
|
318
|
-
const sections = roadmapMd.split(/(?=^##\s+(?:\d{4}-\d{2}-\d{2}|🎯|Phase|Backlog))/m)
|
|
319
|
-
|
|
320
|
-
for (const section of sections) {
|
|
321
|
-
// Backlog section
|
|
322
|
-
if (section.toLowerCase().includes('backlog')) {
|
|
323
|
-
const items = section.match(/^\s*[-*]\s*\[\s*\]\s*(.+)$/gm) || []
|
|
324
|
-
for (const item of items) {
|
|
325
|
-
const text = item.replace(/^\s*[-*]\s*\[\s*\]\s*/, '').trim()
|
|
326
|
-
if (text && !roadmap.backlog.includes(text)) {
|
|
327
|
-
roadmap.backlog.push(text)
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
continue
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
// Feature section
|
|
334
|
-
const headerMatch = section.match(/^##\s+(\d{4}-\d{2}-\d{2})\s+-\s+(.+)$/m)
|
|
335
|
-
if (!headerMatch) continue
|
|
336
|
-
|
|
337
|
-
const dateStr = headerMatch[1]
|
|
338
|
-
const name = headerMatch[2].trim()
|
|
339
|
-
|
|
340
|
-
const impactMatch = section.match(/Impact:\s*\*?\*?(\w+)\*?\*?/i)
|
|
341
|
-
const effortMatch = section.match(/Effort:\s*(.+?)(?:\n|$)/i)
|
|
342
|
-
|
|
343
|
-
let status = 'planned'
|
|
344
|
-
const statusMatch = section.match(/Status:\s*(\w+)/i)
|
|
345
|
-
if (statusMatch) {
|
|
346
|
-
const raw = statusMatch[1].toLowerCase()
|
|
347
|
-
if (raw === 'active' || raw === 'in_progress') status = 'active'
|
|
348
|
-
else if (raw === 'completed' || raw === 'done') status = 'completed'
|
|
349
|
-
else if (raw === 'shipped') status = 'shipped'
|
|
350
|
-
}
|
|
351
|
-
if (section.includes('✅ COMPLETED') || section.includes('✅ Done')) status = 'completed'
|
|
352
|
-
|
|
353
|
-
// Extract tasks
|
|
354
|
-
const tasks = []
|
|
355
|
-
const taskMatches = section.matchAll(/^\s*[-*]?\s*(\d+\.\s*)?\[([x ])\]\s*(.+)$/gm)
|
|
356
|
-
for (const match of taskMatches) {
|
|
357
|
-
const completed = match[2].toLowerCase() === 'x'
|
|
358
|
-
tasks.push({
|
|
359
|
-
id: `task_${generateId()}`,
|
|
360
|
-
description: match[3].trim(),
|
|
361
|
-
completed,
|
|
362
|
-
...(completed && { completedAt: new Date().toISOString() }),
|
|
363
|
-
})
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
const completedCount = tasks.filter(t => t.completed).length
|
|
367
|
-
const progress = tasks.length > 0 ? Math.round((completedCount / tasks.length) * 100) : 0
|
|
368
|
-
|
|
369
|
-
roadmap.features.push({
|
|
370
|
-
id: `feat_${generateId()}`,
|
|
371
|
-
name,
|
|
372
|
-
status,
|
|
373
|
-
impact: impactMatch ? impactMatch[1].toLowerCase() : 'medium',
|
|
374
|
-
...(effortMatch && { effort: effortMatch[1].trim() }),
|
|
375
|
-
progress,
|
|
376
|
-
tasks,
|
|
377
|
-
createdAt: `${dateStr}T00:00:00.000Z`,
|
|
378
|
-
...(status === 'active' && { startedAt: `${dateStr}T00:00:00.000Z` }),
|
|
379
|
-
...(status === 'completed' && { completedAt: new Date().toISOString() }),
|
|
380
|
-
...(status === 'shipped' && { shippedAt: new Date().toISOString() }),
|
|
381
|
-
})
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
return roadmap
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
/**
|
|
389
|
-
* Parse shipped from shipped.md + shipped.json
|
|
390
|
-
*/
|
|
391
|
-
async function parseShipped(projectPath) {
|
|
392
|
-
const shippedMd = await readFile(path.join(projectPath, 'progress', 'shipped.md'))
|
|
393
|
-
const shippedJson = await readJson(path.join(projectPath, 'progress', 'shipped.json'))
|
|
394
|
-
|
|
395
|
-
const shipped = {
|
|
396
|
-
items: [],
|
|
397
|
-
lastUpdated: new Date().toISOString(),
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
if (shippedMd) {
|
|
401
|
-
const sections = shippedMd.split(/(?=^##\s+\d{4}-\d{2}-\d{2})/m)
|
|
402
|
-
|
|
403
|
-
for (const section of sections) {
|
|
404
|
-
const dateMatch = section.match(/^##\s+(\d{4}-\d{2}-\d{2})/)
|
|
405
|
-
if (!dateMatch) continue
|
|
406
|
-
|
|
407
|
-
const dateStr = dateMatch[1]
|
|
408
|
-
const itemMatches = section.matchAll(/[-*]\s*✅\s*\*\*(.+?)\*\*(?:\s*\((.+?)\))?/g)
|
|
409
|
-
|
|
410
|
-
for (const match of itemMatches) {
|
|
411
|
-
const name = match[1].trim()
|
|
412
|
-
const versionInfo = match[2]
|
|
413
|
-
|
|
414
|
-
const versionMatch = versionInfo?.match(/v?([\d.]+)/)
|
|
415
|
-
const version = versionMatch ? `v${versionMatch[1]}` : undefined
|
|
416
|
-
|
|
417
|
-
// Extract changes
|
|
418
|
-
const itemStart = match.index
|
|
419
|
-
const nextItem = section.indexOf('- ✅', itemStart + 1)
|
|
420
|
-
const itemSection = section.substring(itemStart, nextItem > 0 ? nextItem : undefined)
|
|
421
|
-
|
|
422
|
-
const changes = []
|
|
423
|
-
const changeMatches = itemSection.matchAll(/^\s+-\s+(?!Lint:|Tests:|Type:)(.+)$/gm)
|
|
424
|
-
for (const change of changeMatches) {
|
|
425
|
-
changes.push(change[1].trim())
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
// Metrics
|
|
429
|
-
const lintMatch = itemSection.match(/Lint:\s*(\w+)/)
|
|
430
|
-
const testMatch = itemSection.match(/Tests:\s*(\w+)/)
|
|
431
|
-
const metrics = (lintMatch || testMatch) ? {
|
|
432
|
-
lintStatus: normalizeStatus(lintMatch?.[1]),
|
|
433
|
-
testStatus: normalizeStatus(testMatch?.[1]),
|
|
434
|
-
} : undefined
|
|
435
|
-
|
|
436
|
-
// Type
|
|
437
|
-
let type = 'feature'
|
|
438
|
-
const nameLower = name.toLowerCase()
|
|
439
|
-
if (nameLower.includes('fix')) type = 'fix'
|
|
440
|
-
else if (nameLower.includes('refactor')) type = 'refactor'
|
|
441
|
-
else if (nameLower.includes('improvement') || nameLower.includes('optimization')) type = 'improvement'
|
|
442
|
-
|
|
443
|
-
shipped.items.push({
|
|
444
|
-
id: `ship_${generateId()}`,
|
|
445
|
-
name,
|
|
446
|
-
...(version && { version }),
|
|
447
|
-
type,
|
|
448
|
-
changes,
|
|
449
|
-
...(metrics && { metrics }),
|
|
450
|
-
shippedAt: `${dateStr}T12:00:00.000Z`,
|
|
451
|
-
})
|
|
452
|
-
}
|
|
453
|
-
}
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
// Merge from JSON
|
|
457
|
-
if (shippedJson?.items) {
|
|
458
|
-
for (const item of shippedJson.items) {
|
|
459
|
-
const exists = shipped.items.some(s =>
|
|
460
|
-
s.name.toLowerCase() === item.name?.toLowerCase()
|
|
461
|
-
)
|
|
462
|
-
if (!exists && item.name) {
|
|
463
|
-
shipped.items.push({
|
|
464
|
-
id: `ship_${generateId()}`,
|
|
465
|
-
name: item.name,
|
|
466
|
-
type: item.type || 'feature',
|
|
467
|
-
changes: [],
|
|
468
|
-
shippedAt: item.shippedAt || new Date().toISOString(),
|
|
469
|
-
})
|
|
470
|
-
}
|
|
471
|
-
}
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
return shipped
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
/**
|
|
478
|
-
* Parse metrics from metrics.md
|
|
479
|
-
*/
|
|
480
|
-
async function parseMetrics(projectPath) {
|
|
481
|
-
const metricsMd = await readFile(path.join(projectPath, 'progress', 'metrics.md'))
|
|
482
|
-
|
|
483
|
-
const metrics = {
|
|
484
|
-
current: {
|
|
485
|
-
tasksStarted: 0,
|
|
486
|
-
tasksCompleted: 0,
|
|
487
|
-
inProgress: 0,
|
|
488
|
-
},
|
|
489
|
-
allTime: {
|
|
490
|
-
featuresShipped: 0,
|
|
491
|
-
tasksCompleted: 0,
|
|
492
|
-
totalTimeTracked: '0m',
|
|
493
|
-
daysActive: 0,
|
|
494
|
-
},
|
|
495
|
-
velocity: {
|
|
496
|
-
featuresPerWeek: 0,
|
|
497
|
-
tasksPerDay: 0,
|
|
498
|
-
},
|
|
499
|
-
lastActivity: [],
|
|
500
|
-
lastUpdated: new Date().toISOString(),
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
if (!metricsMd) return metrics
|
|
504
|
-
|
|
505
|
-
// Parse current sprint
|
|
506
|
-
const startedMatch = metricsMd.match(/Tasks Started\*\*:\s*(\d+)/)
|
|
507
|
-
const completedMatch = metricsMd.match(/Tasks Completed\*\*:\s*(\d+)/)
|
|
508
|
-
const inProgressMatch = metricsMd.match(/In Progress\*\*:\s*(\d+)/)
|
|
509
|
-
|
|
510
|
-
if (startedMatch) metrics.current.tasksStarted = parseInt(startedMatch[1])
|
|
511
|
-
if (completedMatch) metrics.current.tasksCompleted = parseInt(completedMatch[1])
|
|
512
|
-
if (inProgressMatch) metrics.current.inProgress = parseInt(inProgressMatch[1])
|
|
513
|
-
|
|
514
|
-
// Parse all-time
|
|
515
|
-
const featuresMatch = metricsMd.match(/Features Shipped\*\*:\s*(\d+)/)
|
|
516
|
-
const totalTasksMatch = metricsMd.match(/Tasks Completed\*\*:\s*(\d+)/g)
|
|
517
|
-
const timeMatch = metricsMd.match(/Total Time Tracked\*\*:\s*(.+?)(?:\n|$)/)
|
|
518
|
-
const daysMatch = metricsMd.match(/Days Active\*\*:\s*(\d+)/)
|
|
519
|
-
|
|
520
|
-
if (featuresMatch) metrics.allTime.featuresShipped = parseInt(featuresMatch[1])
|
|
521
|
-
if (totalTasksMatch && totalTasksMatch.length > 1) {
|
|
522
|
-
const match = totalTasksMatch[1].match(/(\d+)/)
|
|
523
|
-
if (match) metrics.allTime.tasksCompleted = parseInt(match[1])
|
|
524
|
-
}
|
|
525
|
-
if (timeMatch) metrics.allTime.totalTimeTracked = timeMatch[1].trim()
|
|
526
|
-
if (daysMatch) metrics.allTime.daysActive = parseInt(daysMatch[1])
|
|
527
|
-
|
|
528
|
-
// Velocity
|
|
529
|
-
const tasksPerDayMatch = metricsMd.match(/Tasks\/Day\*\*:\s*([\d.]+)/)
|
|
530
|
-
if (tasksPerDayMatch) metrics.velocity.tasksPerDay = parseFloat(tasksPerDayMatch[1])
|
|
531
|
-
|
|
532
|
-
// Last activity
|
|
533
|
-
const activityMatches = metricsMd.matchAll(/\*\*(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2})\*\*:\s*(.+?)(?:\n|$)/g)
|
|
534
|
-
for (const match of activityMatches) {
|
|
535
|
-
metrics.lastActivity.push({
|
|
536
|
-
timestamp: match[1],
|
|
537
|
-
description: match[2].trim(),
|
|
538
|
-
})
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
return metrics
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
// ============================================
|
|
545
|
-
// MIGRATION
|
|
546
|
-
// ============================================
|
|
547
|
-
|
|
548
|
-
async function migrateProject(projectId, dryRun = false) {
|
|
549
|
-
const projectPath = path.join(GLOBAL_STORAGE, projectId)
|
|
550
|
-
const dataPath = path.join(projectPath, 'data')
|
|
551
|
-
|
|
552
|
-
const results = {
|
|
553
|
-
migrated: [],
|
|
554
|
-
skipped: [],
|
|
555
|
-
errors: [],
|
|
556
|
-
deleted: [],
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
console.log(`\n${c.bold}${projectId}${c.reset}`)
|
|
560
|
-
|
|
561
|
-
// 1. Parse all data
|
|
562
|
-
const parsers = [
|
|
563
|
-
{ name: 'state', parse: () => parseState(projectPath), file: 'state.json' },
|
|
564
|
-
{ name: 'queue', parse: () => parseQueue(projectPath), file: 'queue.json' },
|
|
565
|
-
{ name: 'ideas', parse: () => parseIdeas(projectPath), file: 'ideas.json' },
|
|
566
|
-
{ name: 'roadmap', parse: () => parseRoadmap(projectPath), file: 'roadmap.json' },
|
|
567
|
-
{ name: 'shipped', parse: () => parseShipped(projectPath), file: 'shipped.json' },
|
|
568
|
-
{ name: 'metrics', parse: () => parseMetrics(projectPath), file: 'metrics.json' },
|
|
569
|
-
]
|
|
570
|
-
|
|
571
|
-
for (const { name, parse, file } of parsers) {
|
|
572
|
-
try {
|
|
573
|
-
const data = await parse()
|
|
574
|
-
const targetPath = path.join(dataPath, file)
|
|
575
|
-
|
|
576
|
-
if (dryRun) {
|
|
577
|
-
console.log(` ${c.dim}→ ${name} → ${file}${c.reset}`)
|
|
578
|
-
} else {
|
|
579
|
-
await writeJson(targetPath, data)
|
|
580
|
-
console.log(` ${c.green}✓ ${name}${c.reset}`)
|
|
581
|
-
}
|
|
582
|
-
results.migrated.push(name)
|
|
583
|
-
} catch (err) {
|
|
584
|
-
console.log(` ${c.red}✗ ${name}: ${err.message}${c.reset}`)
|
|
585
|
-
results.errors.push(`${name}: ${err.message}`)
|
|
586
|
-
}
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
// 2. Copy project.json if exists
|
|
590
|
-
const projectJson = await readJson(path.join(projectPath, 'project.json'))
|
|
591
|
-
if (projectJson) {
|
|
592
|
-
if (!dryRun) {
|
|
593
|
-
await writeJson(path.join(dataPath, 'project.json'), projectJson)
|
|
594
|
-
}
|
|
595
|
-
console.log(` ${c.green}✓ project${c.reset}`)
|
|
596
|
-
results.migrated.push('project')
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
// 3. Delete legacy files (only if not dry run and migration successful)
|
|
600
|
-
if (!dryRun && results.errors.length === 0) {
|
|
601
|
-
const filesToDelete = [
|
|
602
|
-
// Core
|
|
603
|
-
path.join(projectPath, 'core', 'now.md'),
|
|
604
|
-
path.join(projectPath, 'core', 'now.json'),
|
|
605
|
-
path.join(projectPath, 'core', 'next.md'),
|
|
606
|
-
path.join(projectPath, 'core', 'next.json'),
|
|
607
|
-
path.join(projectPath, 'core', 'context.md'),
|
|
608
|
-
// Planning
|
|
609
|
-
path.join(projectPath, 'planning', 'ideas.md'),
|
|
610
|
-
path.join(projectPath, 'planning', 'ideas.json'),
|
|
611
|
-
path.join(projectPath, 'planning', 'roadmap.md'),
|
|
612
|
-
path.join(projectPath, 'planning', 'roadmap.json'),
|
|
613
|
-
// Progress
|
|
614
|
-
path.join(projectPath, 'progress', 'shipped.md'),
|
|
615
|
-
path.join(projectPath, 'progress', 'shipped.json'),
|
|
616
|
-
path.join(projectPath, 'progress', 'metrics.md'),
|
|
617
|
-
// Root
|
|
618
|
-
path.join(projectPath, 'project.json'),
|
|
619
|
-
]
|
|
620
|
-
|
|
621
|
-
for (const file of filesToDelete) {
|
|
622
|
-
if (await deleteFile(file)) {
|
|
623
|
-
results.deleted.push(path.basename(file))
|
|
624
|
-
}
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
// Try to remove empty directories
|
|
628
|
-
await deleteDirIfEmpty(path.join(projectPath, 'core'))
|
|
629
|
-
await deleteDirIfEmpty(path.join(projectPath, 'planning'))
|
|
630
|
-
await deleteDirIfEmpty(path.join(projectPath, 'progress'))
|
|
631
|
-
|
|
632
|
-
if (results.deleted.length > 0) {
|
|
633
|
-
console.log(` ${c.yellow}🗑 Deleted: ${results.deleted.length} legacy files${c.reset}`)
|
|
634
|
-
}
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
return results
|
|
638
|
-
}
|
|
639
|
-
|
|
640
|
-
// ============================================
|
|
641
|
-
// CLI
|
|
642
|
-
// ============================================
|
|
643
|
-
|
|
644
|
-
function parseArgs(argv) {
|
|
645
|
-
const args = { project: null, all: false, dryRun: false, help: false }
|
|
646
|
-
for (const arg of argv.slice(2)) {
|
|
647
|
-
if (arg === '--help' || arg === '-h') args.help = true
|
|
648
|
-
else if (arg === '--all') args.all = true
|
|
649
|
-
else if (arg === '--dry-run') args.dryRun = true
|
|
650
|
-
else if (arg.startsWith('--project=')) args.project = arg.split('=')[1]
|
|
651
|
-
}
|
|
652
|
-
return args
|
|
653
|
-
}
|
|
654
|
-
|
|
655
|
-
function printHelp() {
|
|
656
|
-
console.log(`
|
|
657
|
-
${c.cyan}${c.bold}prjct migrate-to-json${c.reset}
|
|
658
|
-
|
|
659
|
-
Complete migration to JSON-first architecture.
|
|
660
|
-
|
|
661
|
-
${c.bold}Usage:${c.reset}
|
|
662
|
-
prjct migrate-to-json --project=<id> Migrate specific project
|
|
663
|
-
prjct migrate-to-json --all Migrate all projects
|
|
664
|
-
prjct migrate-to-json --dry-run Preview without writing
|
|
665
|
-
|
|
666
|
-
${c.bold}What Gets Migrated:${c.reset}
|
|
667
|
-
core/now.md + now.json → data/state.json
|
|
668
|
-
core/next.md + next.json → data/queue.json
|
|
669
|
-
planning/ideas.md + .json → data/ideas.json
|
|
670
|
-
planning/roadmap.md + .json → data/roadmap.json
|
|
671
|
-
progress/shipped.md + .json → data/shipped.json
|
|
672
|
-
progress/metrics.md → data/metrics.json
|
|
673
|
-
project.json → data/project.json
|
|
674
|
-
|
|
675
|
-
${c.bold}After Migration:${c.reset}
|
|
676
|
-
Legacy files are automatically deleted.
|
|
677
|
-
Run 'prjct generate-views --all' to create MD views.
|
|
678
|
-
`)
|
|
679
|
-
}
|
|
680
|
-
|
|
681
|
-
async function getAllProjects() {
|
|
682
|
-
try {
|
|
683
|
-
const entries = await fs.readdir(GLOBAL_STORAGE, { withFileTypes: true })
|
|
684
|
-
return entries.filter(e => e.isDirectory()).map(e => e.name)
|
|
685
|
-
} catch {
|
|
686
|
-
return []
|
|
687
|
-
}
|
|
688
|
-
}
|
|
689
|
-
|
|
690
|
-
async function main() {
|
|
691
|
-
const args = parseArgs(process.argv)
|
|
692
|
-
|
|
693
|
-
if (args.help) {
|
|
694
|
-
printHelp()
|
|
695
|
-
process.exit(0)
|
|
696
|
-
}
|
|
697
|
-
|
|
698
|
-
if (!args.project && !args.all) {
|
|
699
|
-
console.log(`${c.red}Error: Specify --project=<id> or --all${c.reset}`)
|
|
700
|
-
process.exit(1)
|
|
701
|
-
}
|
|
702
|
-
|
|
703
|
-
const projects = args.all ? await getAllProjects() : [args.project]
|
|
704
|
-
if (projects.length === 0) {
|
|
705
|
-
console.log(`${c.yellow}No projects found${c.reset}`)
|
|
706
|
-
process.exit(0)
|
|
707
|
-
}
|
|
708
|
-
|
|
709
|
-
console.log(`${c.cyan}${c.bold}${args.dryRun ? '[DRY RUN] ' : ''}Migrating to JSON-first...${c.reset}`)
|
|
710
|
-
|
|
711
|
-
let totalMigrated = 0
|
|
712
|
-
let totalErrors = 0
|
|
713
|
-
|
|
714
|
-
for (const projectId of projects) {
|
|
715
|
-
try {
|
|
716
|
-
await fs.access(path.join(GLOBAL_STORAGE, projectId))
|
|
717
|
-
const result = await migrateProject(projectId, args.dryRun)
|
|
718
|
-
totalMigrated += result.migrated.length
|
|
719
|
-
totalErrors += result.errors.length
|
|
720
|
-
} catch {
|
|
721
|
-
console.log(`${c.yellow}⚠ Project not found: ${projectId}${c.reset}`)
|
|
722
|
-
}
|
|
723
|
-
}
|
|
724
|
-
|
|
725
|
-
console.log('')
|
|
726
|
-
if (args.dryRun) {
|
|
727
|
-
console.log(`${c.yellow}Dry run complete. No files were written.${c.reset}`)
|
|
728
|
-
} else if (totalMigrated > 0) {
|
|
729
|
-
console.log(`${c.green}${c.bold}✓ Migrated ${totalMigrated} items${c.reset}`)
|
|
730
|
-
console.log(`${c.dim}Run 'prjct generate-views --all' to create MD views${c.reset}`)
|
|
731
|
-
}
|
|
732
|
-
|
|
733
|
-
if (totalErrors > 0) {
|
|
734
|
-
console.log(`${c.red}✗ ${totalErrors} error(s)${c.reset}`)
|
|
735
|
-
process.exit(1)
|
|
736
|
-
}
|
|
737
|
-
}
|
|
738
|
-
|
|
739
|
-
main().catch(err => {
|
|
740
|
-
console.error(`${c.red}Error: ${err.message}${c.reset}`)
|
|
741
|
-
process.exit(1)
|
|
742
|
-
})
|