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.
Files changed (72) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/bin/dev.js +0 -1
  3. package/bin/serve.js +19 -20
  4. package/core/__tests__/agentic/memory-system.test.ts +2 -1
  5. package/core/__tests__/agentic/plan-mode.test.ts +2 -1
  6. package/core/agentic/agent-router.ts +79 -14
  7. package/core/agentic/command-executor/command-executor.ts +2 -74
  8. package/core/agentic/services.ts +0 -48
  9. package/core/agentic/template-loader.ts +35 -1
  10. package/core/command-registry/setup-commands.ts +15 -0
  11. package/core/commands/base.ts +96 -77
  12. package/core/commands/planning.ts +13 -2
  13. package/core/commands/setup.ts +3 -85
  14. package/core/domain/agent-generator.ts +9 -17
  15. package/core/errors.ts +209 -0
  16. package/core/infrastructure/config-manager.ts +22 -5
  17. package/core/infrastructure/path-manager.ts +23 -1
  18. package/core/infrastructure/setup.ts +5 -50
  19. package/core/storage/ideas-storage.ts +4 -0
  20. package/core/storage/queue-storage.ts +4 -0
  21. package/core/storage/shipped-storage.ts +4 -0
  22. package/core/storage/state-storage.ts +4 -0
  23. package/core/storage/storage-manager.ts +52 -13
  24. package/core/sync/auth-config.ts +145 -0
  25. package/core/sync/index.ts +30 -0
  26. package/core/sync/oauth-handler.ts +148 -0
  27. package/core/sync/sync-client.ts +252 -0
  28. package/core/sync/sync-manager.ts +358 -0
  29. package/core/utils/logger.ts +19 -12
  30. package/package.json +2 -4
  31. package/templates/agentic/subagent-generation.md +109 -0
  32. package/templates/commands/auth.md +234 -0
  33. package/templates/commands/sync.md +129 -13
  34. package/templates/subagents/domain/backend.md +105 -0
  35. package/templates/subagents/domain/database.md +118 -0
  36. package/templates/subagents/domain/devops.md +148 -0
  37. package/templates/subagents/domain/frontend.md +99 -0
  38. package/templates/subagents/domain/testing.md +169 -0
  39. package/templates/subagents/workflow/prjct-planner.md +158 -0
  40. package/templates/subagents/workflow/prjct-shipper.md +179 -0
  41. package/templates/subagents/workflow/prjct-workflow.md +98 -0
  42. package/bin/generate-views.js +0 -209
  43. package/bin/migrate-to-json.js +0 -742
  44. package/core/agentic/context-filter.ts +0 -365
  45. package/core/agentic/parallel-tools.ts +0 -165
  46. package/core/agentic/response-templates.ts +0 -164
  47. package/core/agentic/semantic-compression.ts +0 -273
  48. package/core/agentic/think-blocks.ts +0 -202
  49. package/core/agentic/validation-rules.ts +0 -313
  50. package/core/domain/agent-matcher.ts +0 -130
  51. package/core/domain/agent-validator.ts +0 -250
  52. package/core/domain/architect-session.ts +0 -315
  53. package/core/domain/product-standards.ts +0 -106
  54. package/core/domain/smart-cache.ts +0 -167
  55. package/core/domain/task-analyzer.ts +0 -296
  56. package/core/infrastructure/legacy-installer-detector/cleanup.ts +0 -216
  57. package/core/infrastructure/legacy-installer-detector/detection.ts +0 -95
  58. package/core/infrastructure/legacy-installer-detector/index.ts +0 -171
  59. package/core/infrastructure/legacy-installer-detector/migration.ts +0 -87
  60. package/core/infrastructure/legacy-installer-detector/types.ts +0 -42
  61. package/core/infrastructure/legacy-installer-detector.ts +0 -7
  62. package/core/infrastructure/migrator/file-operations.ts +0 -125
  63. package/core/infrastructure/migrator/index.ts +0 -288
  64. package/core/infrastructure/migrator/project-scanner.ts +0 -90
  65. package/core/infrastructure/migrator/reports.ts +0 -117
  66. package/core/infrastructure/migrator/types.ts +0 -124
  67. package/core/infrastructure/migrator/validation.ts +0 -94
  68. package/core/infrastructure/migrator/version-migration.ts +0 -117
  69. package/core/infrastructure/migrator.ts +0 -10
  70. package/core/infrastructure/uuid-migration.ts +0 -750
  71. package/templates/commands/migrate-all.md +0 -96
  72. package/templates/commands/migrate.md +0 -140
@@ -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
- })