prjct-cli 0.12.2 → 0.13.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 +43 -0
- package/CLAUDE.md +18 -6
- package/core/data/index.ts +19 -5
- package/core/data/md-base-manager.ts +203 -0
- package/core/data/md-queue-manager.ts +179 -0
- package/core/data/md-state-manager.ts +133 -0
- package/core/serializers/index.ts +20 -0
- package/core/serializers/queue-serializer.ts +210 -0
- package/core/serializers/state-serializer.ts +136 -0
- package/core/utils/file-helper.ts +12 -0
- package/package.json +1 -1
- package/packages/web/app/api/projects/[id]/stats/route.ts +6 -29
- package/packages/web/app/page.tsx +1 -6
- package/packages/web/app/project/[id]/page.tsx +34 -1
- package/packages/web/app/project/[id]/stats/page.tsx +11 -5
- package/packages/web/app/settings/page.tsx +2 -221
- package/packages/web/components/BlockersCard/BlockersCard.tsx +67 -0
- package/packages/web/components/BlockersCard/BlockersCard.types.ts +11 -0
- package/packages/web/components/BlockersCard/index.ts +2 -0
- package/packages/web/components/CommandButton/CommandButton.tsx +10 -3
- package/packages/web/lib/projects.ts +28 -27
- package/packages/web/lib/services/projects.server.ts +25 -21
- package/packages/web/lib/services/stats.server.ts +355 -57
- package/packages/web/next-env.d.ts +1 -1
- package/packages/web/package.json +0 -4
- package/templates/commands/decision.md +226 -0
- package/templates/commands/done.md +100 -68
- package/templates/commands/feature.md +102 -103
- package/templates/commands/idea.md +41 -38
- package/templates/commands/now.md +94 -33
- package/templates/commands/pause.md +90 -30
- package/templates/commands/ship.md +179 -74
- package/templates/commands/sync.md +324 -200
- package/packages/web/app/api/migrate/route.ts +0 -46
- package/packages/web/app/api/settings/route.ts +0 -97
- package/packages/web/app/api/v2/projects/[id]/unified/route.ts +0 -57
- package/packages/web/components/MigrationGate/MigrationGate.tsx +0 -304
- package/packages/web/components/MigrationGate/index.ts +0 -1
- package/packages/web/lib/json-loader.ts +0 -630
- package/packages/web/lib/services/migration.server.ts +0 -580
|
@@ -1,580 +0,0 @@
|
|
|
1
|
-
import { createOpenAI } from '@ai-sdk/openai'
|
|
2
|
-
import { generateText } from 'ai'
|
|
3
|
-
import { promises as fs } from 'fs'
|
|
4
|
-
import { join } from 'path'
|
|
5
|
-
import { homedir } from 'os'
|
|
6
|
-
|
|
7
|
-
const SETTINGS_PATH = join(homedir(), '.prjct-cli', 'settings.json')
|
|
8
|
-
const GLOBAL_STORAGE = join(homedir(), '.prjct-cli', 'projects')
|
|
9
|
-
|
|
10
|
-
// Complete JSON Schema definitions for new architecture
|
|
11
|
-
// JSON is source of truth, MD is generated for Claude
|
|
12
|
-
// ENRICHED SCHEMAS - Extract all rich data from MD files
|
|
13
|
-
const SCHEMAS = {
|
|
14
|
-
state: `{
|
|
15
|
-
"currentTask": {
|
|
16
|
-
"id": "task_xxxxxxxx - unique ID",
|
|
17
|
-
"description": "string",
|
|
18
|
-
"startedAt": "ISO-8601 timestamp",
|
|
19
|
-
"sessionId": "sess_xxxxxxxx",
|
|
20
|
-
"featureId": "feat_xxxxxxxx (optional)"
|
|
21
|
-
} | null,
|
|
22
|
-
"previousTask": {
|
|
23
|
-
"id": "string",
|
|
24
|
-
"description": "string",
|
|
25
|
-
"status": "paused",
|
|
26
|
-
"startedAt": "ISO-8601",
|
|
27
|
-
"pausedAt": "ISO-8601"
|
|
28
|
-
} | null (optional - for paused tasks),
|
|
29
|
-
"lastUpdated": "ISO-8601 timestamp"
|
|
30
|
-
}`,
|
|
31
|
-
|
|
32
|
-
queue: `{
|
|
33
|
-
"tasks": [{
|
|
34
|
-
"id": "task_xxxxxxxx",
|
|
35
|
-
"description": "string",
|
|
36
|
-
"priority": "critical|high|medium|low",
|
|
37
|
-
"type": "feature|bug|improvement|chore (detect from emoji 🐛=bug)",
|
|
38
|
-
"featureId": "feat_xxx (optional)",
|
|
39
|
-
"originFeature": "string - from (from: Feature Name) pattern (optional)",
|
|
40
|
-
"completed": boolean,
|
|
41
|
-
"completedAt": "ISO-8601 (if completed)",
|
|
42
|
-
"createdAt": "ISO-8601",
|
|
43
|
-
"section": "active|backlog|previously_active (based on MD section)",
|
|
44
|
-
"agent": "fe|be|fe + be (extract from **Agent**: pattern in task group)",
|
|
45
|
-
"groupName": "string - group/section name like 'Sales Reports', 'Stock Audits'",
|
|
46
|
-
"groupId": "string - unique ID for the group (optional)"
|
|
47
|
-
}],
|
|
48
|
-
"lastUpdated": "ISO-8601"
|
|
49
|
-
}`,
|
|
50
|
-
|
|
51
|
-
ideas: `{
|
|
52
|
-
"ideas": [{
|
|
53
|
-
"id": "idea_xxxxxxxx",
|
|
54
|
-
"text": "title/summary",
|
|
55
|
-
"details": "expanded description (optional)",
|
|
56
|
-
"priority": "high|medium|low",
|
|
57
|
-
"status": "pending|converted|completed|archived",
|
|
58
|
-
"tags": ["array", "of", "tags"],
|
|
59
|
-
"addedAt": "ISO-8601",
|
|
60
|
-
"completedAt": "ISO-8601 (if status=completed, extract from 'COMPLETED YYYY-MM-DD')",
|
|
61
|
-
"convertedTo": "feat_xxx (if status=converted)",
|
|
62
|
-
"source": "docs/technical-spec-v1.md, docs/edr-v1.md (from **Source**: pattern)",
|
|
63
|
-
"sourceFiles": ["array of source file paths"],
|
|
64
|
-
"painPoints": ["array of pain points from ### Pain Points or ### Riesgos section"],
|
|
65
|
-
"solutions": ["array of solutions from ### Solutions section"],
|
|
66
|
-
"filesAffected": ["array of file paths from **Files:** section"],
|
|
67
|
-
"impactEffort": {"impact": "high|medium|low", "effort": "high|medium|low"} (optional),
|
|
68
|
-
"stack": {
|
|
69
|
-
"frontend": "Next.js 14, HeroUI",
|
|
70
|
-
"backend": "Supabase (Auth, DB, RLS, Realtime)",
|
|
71
|
-
"payments": "Stripe Billing",
|
|
72
|
-
"ai": "Vercel AI SDK",
|
|
73
|
-
"deploy": "Vercel"
|
|
74
|
-
} (extract from ### Stack section),
|
|
75
|
-
"modules": [{"name": "Multi-tenant", "description": "Empresas con RLS estricto"}] (from ### Módulos section),
|
|
76
|
-
"roles": [{"name": "SUPER_ADMIN", "description": "(global, impersonation)"}] (from ### Roles section),
|
|
77
|
-
"risks": ["array of risks from ### Riesgos Críticos section"],
|
|
78
|
-
"risksCount": number (from '33 pitfalls documented')
|
|
79
|
-
}],
|
|
80
|
-
"lastUpdated": "ISO-8601"
|
|
81
|
-
}`,
|
|
82
|
-
|
|
83
|
-
roadmap: `{
|
|
84
|
-
"strategy": {
|
|
85
|
-
"goal": "strategic goal string (optional)",
|
|
86
|
-
"phases": [{"id": "P0", "name": "string", "status": "completed|active|planned", "completedAt": "ISO"}],
|
|
87
|
-
"successMetrics": ["array of KPIs"]
|
|
88
|
-
} | null (optional),
|
|
89
|
-
"features": [{
|
|
90
|
-
"id": "feat_xxxxxxxx",
|
|
91
|
-
"name": "string",
|
|
92
|
-
"description": "string (optional)",
|
|
93
|
-
"date": "YYYY-MM-DD creation date",
|
|
94
|
-
"impact": "high|medium|low (from Impact: HIGH)",
|
|
95
|
-
"effort": "1-2 days or similar string",
|
|
96
|
-
"status": "planned|active|completed|shipped",
|
|
97
|
-
"progress": 0-100 (calculate from tasks completed/total),
|
|
98
|
-
"type": "feature|breaking_change|refactor|infrastructure (from Type: BREAKING CHANGE)",
|
|
99
|
-
"roi": 1-5 (count ⭐ stars, optional),
|
|
100
|
-
"why": ["array from ### Why This Feature? section"],
|
|
101
|
-
"technicalNotes": ["array from ### Technical Notes section"],
|
|
102
|
-
"compatibility": "string (optional)",
|
|
103
|
-
"phase": "P0|P1|P2|P3 (optional)",
|
|
104
|
-
"tasks": [{
|
|
105
|
-
"id": "task_xxxxxxxx",
|
|
106
|
-
"description": "string",
|
|
107
|
-
"completed": boolean ([ ]=false, [x]=true),
|
|
108
|
-
"completedAt": "ISO-8601 (if completed)"
|
|
109
|
-
}],
|
|
110
|
-
"createdAt": "ISO-8601",
|
|
111
|
-
"shippedAt": "ISO-8601 (if shipped)",
|
|
112
|
-
"version": "0.11.6 (extract from v0.11.6 pattern)",
|
|
113
|
-
"duration": {"hours": number, "minutes": number, "totalMinutes": number, "display": "~25m"} (parse from '~25m', '~1h'),
|
|
114
|
-
"taskCount": number (extract from '7 tasks' in header),
|
|
115
|
-
"agent": "fe+be|fe|be (from **Agent**: pattern)",
|
|
116
|
-
"sprintName": "Sprint 6 - Reports + Audits (full sprint name)",
|
|
117
|
-
"completedDate": "2025-12-09 (exact completion date from MD)"
|
|
118
|
-
}],
|
|
119
|
-
"backlog": ["string array"],
|
|
120
|
-
"lastUpdated": "ISO-8601"
|
|
121
|
-
}`,
|
|
122
|
-
|
|
123
|
-
shipped: `{
|
|
124
|
-
"items": [{
|
|
125
|
-
"id": "ship_xxxxxxxx",
|
|
126
|
-
"name": "string",
|
|
127
|
-
"version": "0.11.6 (extract from (v0.11.6) or v0.11.6 patterns, null if none)",
|
|
128
|
-
"type": "feature|fix|improvement|refactor",
|
|
129
|
-
"agent": "fe|be|fe+be|devops|ai (extract from **Agent**: pattern)",
|
|
130
|
-
"description": "string - full narrative description text (NOT bullet points)",
|
|
131
|
-
"changes": [{"description": "string", "type": "added|changed|fixed|removed"}] (from bullet points),
|
|
132
|
-
"codeSnippets": ["string array of code blocks if any"] (optional),
|
|
133
|
-
"commit": {
|
|
134
|
-
"hash": "0a7bbea (short hash from **Commit**: pattern)",
|
|
135
|
-
"message": "feat(security): Multi-tenant... (commit message)"
|
|
136
|
-
} (optional),
|
|
137
|
-
"codeMetrics": {
|
|
138
|
-
"filesChanged": number|null,
|
|
139
|
-
"linesAdded": number|null,
|
|
140
|
-
"linesRemoved": number|null,
|
|
141
|
-
"commits": number|null
|
|
142
|
-
} (extract from 'Files: 4 | +160/-31 | Commits: 0'),
|
|
143
|
-
"qualityMetrics": {
|
|
144
|
-
"lintStatus": "pass|warning|fail|skipped|null",
|
|
145
|
-
"lintDetails": "string (optional)",
|
|
146
|
-
"testStatus": "pass|warning|fail|skipped|null",
|
|
147
|
-
"testDetails": "string - e.g. (2 date-helper tests)"
|
|
148
|
-
} (extract from 'Lint: warnings | Tests: failed (details)'),
|
|
149
|
-
"quantitativeImpact": "81% (1,079 → 204 lines) (optional)",
|
|
150
|
-
"duration": {"hours": number, "minutes": number, "totalMinutes": number} (parse from '~45m', '~1h', '13h 38m'),
|
|
151
|
-
"tasksCompleted": number|null (extract from 'Tasks: 6' or '6 tasks'),
|
|
152
|
-
"shippedAt": "ISO-8601",
|
|
153
|
-
"featureId": "feat_xxx (optional)"
|
|
154
|
-
}],
|
|
155
|
-
"lastUpdated": "ISO-8601"
|
|
156
|
-
}`,
|
|
157
|
-
|
|
158
|
-
metrics: `{
|
|
159
|
-
"currentSprint": {
|
|
160
|
-
"tasksStarted": number,
|
|
161
|
-
"tasksCompleted": number,
|
|
162
|
-
"inProgress": number
|
|
163
|
-
},
|
|
164
|
-
"allTime": {
|
|
165
|
-
"featuresShipped": number,
|
|
166
|
-
"tasksCompleted": number,
|
|
167
|
-
"totalTimeTracked": {"hours": number, "minutes": number, "totalMinutes": number},
|
|
168
|
-
"daysActive": number
|
|
169
|
-
},
|
|
170
|
-
"velocity": {
|
|
171
|
-
"featuresPerWeek": number,
|
|
172
|
-
"tasksPerDay": number
|
|
173
|
-
},
|
|
174
|
-
"recentActivity": [{
|
|
175
|
-
"timestamp": "ISO-8601",
|
|
176
|
-
"action": "started|completed|shipped|paused",
|
|
177
|
-
"description": "string",
|
|
178
|
-
"duration": {"hours": number, "minutes": number} (optional, parse from '(1m)' or '(7h 39m)'),
|
|
179
|
-
"codeChanges": {"files": number, "added": number, "removed": number, "commits": number} (optional)
|
|
180
|
-
}] (parse from Last Activity section and activity lines),
|
|
181
|
-
"lastUpdated": "ISO-8601"
|
|
182
|
-
}`
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
async function getApiKey(): Promise<string | null> {
|
|
186
|
-
try {
|
|
187
|
-
const content = await fs.readFile(SETTINGS_PATH, 'utf-8')
|
|
188
|
-
const settings = JSON.parse(content)
|
|
189
|
-
return settings.openRouterApiKey || null
|
|
190
|
-
} catch {
|
|
191
|
-
return null
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
async function readMdFile(projectId: string, relativePath: string): Promise<string | null> {
|
|
196
|
-
try {
|
|
197
|
-
const fullPath = join(GLOBAL_STORAGE, projectId, relativePath)
|
|
198
|
-
return await fs.readFile(fullPath, 'utf-8')
|
|
199
|
-
} catch {
|
|
200
|
-
return null
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
async function writeJsonFile(projectId: string, relativePath: string, data: unknown): Promise<void> {
|
|
205
|
-
const fullPath = join(GLOBAL_STORAGE, projectId, relativePath)
|
|
206
|
-
const dir = fullPath.substring(0, fullPath.lastIndexOf('/'))
|
|
207
|
-
await fs.mkdir(dir, { recursive: true })
|
|
208
|
-
await fs.writeFile(fullPath, JSON.stringify(data, null, 2))
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
type MigrationFile = {
|
|
212
|
-
mdPaths: string[] // Multiple source files to read
|
|
213
|
-
jsonPath: string // Destination in data/ directory
|
|
214
|
-
schemaKey: keyof typeof SCHEMAS
|
|
215
|
-
name: string
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
// New architecture: MD → data/*.json
|
|
219
|
-
// All JSON files go to data/ directory
|
|
220
|
-
const MIGRATION_FILES: MigrationFile[] = [
|
|
221
|
-
{
|
|
222
|
-
mdPaths: ['core/now.md', 'core/now.json', 'sessions/current.json'],
|
|
223
|
-
jsonPath: 'data/state.json',
|
|
224
|
-
schemaKey: 'state',
|
|
225
|
-
name: 'state'
|
|
226
|
-
},
|
|
227
|
-
{
|
|
228
|
-
mdPaths: ['core/next.md', 'core/next.json'],
|
|
229
|
-
jsonPath: 'data/queue.json',
|
|
230
|
-
schemaKey: 'queue',
|
|
231
|
-
name: 'queue'
|
|
232
|
-
},
|
|
233
|
-
{
|
|
234
|
-
mdPaths: ['planning/ideas.md', 'planning/ideas.json'],
|
|
235
|
-
jsonPath: 'data/ideas.json',
|
|
236
|
-
schemaKey: 'ideas',
|
|
237
|
-
name: 'ideas'
|
|
238
|
-
},
|
|
239
|
-
{
|
|
240
|
-
mdPaths: ['planning/roadmap.md', 'planning/roadmap.json'],
|
|
241
|
-
jsonPath: 'data/roadmap.json',
|
|
242
|
-
schemaKey: 'roadmap',
|
|
243
|
-
name: 'roadmap'
|
|
244
|
-
},
|
|
245
|
-
{
|
|
246
|
-
mdPaths: ['progress/shipped.md', 'progress/shipped.json'],
|
|
247
|
-
jsonPath: 'data/shipped.json',
|
|
248
|
-
schemaKey: 'shipped',
|
|
249
|
-
name: 'shipped'
|
|
250
|
-
},
|
|
251
|
-
{
|
|
252
|
-
mdPaths: ['progress/metrics.md'],
|
|
253
|
-
jsonPath: 'data/metrics.json',
|
|
254
|
-
schemaKey: 'metrics',
|
|
255
|
-
name: 'metrics'
|
|
256
|
-
}
|
|
257
|
-
]
|
|
258
|
-
|
|
259
|
-
// Legacy files to delete after successful migration
|
|
260
|
-
const LEGACY_FILES = [
|
|
261
|
-
'core/now.md',
|
|
262
|
-
'core/now.json',
|
|
263
|
-
'core/next.md',
|
|
264
|
-
'core/next.json',
|
|
265
|
-
'core/context.md',
|
|
266
|
-
'planning/ideas.md',
|
|
267
|
-
'planning/ideas.json',
|
|
268
|
-
'planning/roadmap.md',
|
|
269
|
-
'planning/roadmap.json',
|
|
270
|
-
'progress/shipped.md',
|
|
271
|
-
'progress/shipped.json',
|
|
272
|
-
'progress/metrics.md',
|
|
273
|
-
]
|
|
274
|
-
|
|
275
|
-
async function deleteLegacyFiles(projectId: string): Promise<string[]> {
|
|
276
|
-
const deleted: string[] = []
|
|
277
|
-
for (const file of LEGACY_FILES) {
|
|
278
|
-
try {
|
|
279
|
-
await fs.unlink(join(GLOBAL_STORAGE, projectId, file))
|
|
280
|
-
deleted.push(file)
|
|
281
|
-
} catch {
|
|
282
|
-
// File doesn't exist or can't be deleted
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
// Try to remove empty directories
|
|
287
|
-
const dirsToCheck = ['core', 'planning', 'progress']
|
|
288
|
-
for (const dir of dirsToCheck) {
|
|
289
|
-
try {
|
|
290
|
-
const dirPath = join(GLOBAL_STORAGE, projectId, dir)
|
|
291
|
-
const files = await fs.readdir(dirPath)
|
|
292
|
-
if (files.length === 0) {
|
|
293
|
-
await fs.rmdir(dirPath)
|
|
294
|
-
deleted.push(dir + '/')
|
|
295
|
-
}
|
|
296
|
-
} catch {
|
|
297
|
-
// Directory doesn't exist or not empty
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
return deleted
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
export type MigrationResult = {
|
|
305
|
-
file: string
|
|
306
|
-
success: boolean
|
|
307
|
-
error?: string
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
export async function migrateProject(projectId: string, deleteLegacy: boolean = true): Promise<{
|
|
311
|
-
success: boolean
|
|
312
|
-
results: MigrationResult[]
|
|
313
|
-
deletedFiles: string[]
|
|
314
|
-
viewsGenerated?: boolean
|
|
315
|
-
error?: string
|
|
316
|
-
}> {
|
|
317
|
-
const apiKey = await getApiKey()
|
|
318
|
-
if (!apiKey) {
|
|
319
|
-
return { success: false, results: [], deletedFiles: [], error: 'No OpenRouter API key configured' }
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
const openrouter = createOpenAI({
|
|
323
|
-
baseURL: 'https://openrouter.ai/api/v1',
|
|
324
|
-
apiKey
|
|
325
|
-
})
|
|
326
|
-
|
|
327
|
-
const results: MigrationResult[] = []
|
|
328
|
-
|
|
329
|
-
for (const file of MIGRATION_FILES) {
|
|
330
|
-
// Read all source files for this migration
|
|
331
|
-
const sourceContents: string[] = []
|
|
332
|
-
for (const mdPath of file.mdPaths) {
|
|
333
|
-
const content = await readMdFile(projectId, mdPath)
|
|
334
|
-
if (content && content.trim()) {
|
|
335
|
-
sourceContents.push(`## Source: ${mdPath}\n${content}`)
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
if (sourceContents.length === 0) {
|
|
340
|
-
results.push({ file: file.name, success: true, error: 'No source files found, skipped' })
|
|
341
|
-
continue
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
const combinedContent = sourceContents.join('\n\n---\n\n')
|
|
345
|
-
|
|
346
|
-
try {
|
|
347
|
-
const schema = SCHEMAS[file.schemaKey]
|
|
348
|
-
const { text } = await generateText({
|
|
349
|
-
model: openrouter('anthropic/claude-3.5-haiku'),
|
|
350
|
-
prompt: `Parse these source files and extract ALL structured data. Return ONLY valid JSON matching this schema (no markdown, no explanation):
|
|
351
|
-
|
|
352
|
-
## Target Schema:
|
|
353
|
-
${schema}
|
|
354
|
-
|
|
355
|
-
## Source Files Content:
|
|
356
|
-
${combinedContent}
|
|
357
|
-
|
|
358
|
-
## CRITICAL Pattern Extraction Rules - ZERO DATA LOSS:
|
|
359
|
-
|
|
360
|
-
AGENT PATTERNS - Extract agent type:
|
|
361
|
-
- "**Agent**: be" → agent: "be"
|
|
362
|
-
- "**Agent**: fe + be" → agent: "fe+be"
|
|
363
|
-
- "**Agent**: fe" → agent: "fe"
|
|
364
|
-
|
|
365
|
-
COMMIT PATTERNS - Extract git commit info:
|
|
366
|
-
- "**Commit**: \`0a7bbea\` feat(security): Multi-tenant..." → commit: {hash: "0a7bbea", message: "feat(security): Multi-tenant..."}
|
|
367
|
-
- "**Commits**: \`feat(sprint-6): Sales Reports\`, \`feat(sprint-6): Stock audit\`" → extract multiple commit messages
|
|
368
|
-
|
|
369
|
-
DURATION PATTERNS - Parse to {hours, minutes, totalMinutes, display}:
|
|
370
|
-
- "~1m" or "(1m)" → {hours: 0, minutes: 1, totalMinutes: 1, display: "~1m"}
|
|
371
|
-
- "~45m" → {hours: 0, minutes: 45, totalMinutes: 45, display: "~45m"}
|
|
372
|
-
- "~1h" or "(1h)" → {hours: 1, minutes: 0, totalMinutes: 60, display: "~1h"}
|
|
373
|
-
- "(7h 39m)" → {hours: 7, minutes: 39, totalMinutes: 499, display: "7h 39m"}
|
|
374
|
-
- "**Time**: ~28h" → {hours: 28, minutes: 0, totalMinutes: 1680, display: "~28h"}
|
|
375
|
-
|
|
376
|
-
TASK COUNT PATTERNS:
|
|
377
|
-
- "**Tasks**: 7" → taskCount: 7
|
|
378
|
-
- "(7 tasks, ~25m)" → taskCount: 7, duration: {hours: 0, minutes: 25, totalMinutes: 25}
|
|
379
|
-
- "(8 tasks)" → taskCount: 8
|
|
380
|
-
|
|
381
|
-
CODE METRICS - Parse "Files: X | +Y/-Z | Commits: N":
|
|
382
|
-
- "Files: 4 | +160/-31 | Commits: 0" → {filesChanged: 4, linesAdded: 160, linesRemoved: 31, commits: 0}
|
|
383
|
-
- "**Files**: 59" → {filesChanged: 59, linesAdded: null, linesRemoved: null, commits: null}
|
|
384
|
-
|
|
385
|
-
QUALITY STATUS - Parse "Lint: X | Tests: Y":
|
|
386
|
-
- "Lint: warnings" → lintStatus: "warning"
|
|
387
|
-
- "Tests: failed (2 date-helper tests)" → testStatus: "fail", testDetails: "2 date-helper tests"
|
|
388
|
-
|
|
389
|
-
IMPACT/EFFORT - Parse "Impact: X | Effort: Y":
|
|
390
|
-
- "Impact: **HIGH** | Effort: 1-2 days" → impact: "high", effort: "1-2 days"
|
|
391
|
-
|
|
392
|
-
ROI - Count stars:
|
|
393
|
-
- "⭐⭐⭐⭐⭐" → roi: 5
|
|
394
|
-
|
|
395
|
-
TASK TYPE - Detect from emoji:
|
|
396
|
-
- "🐛 [HIGH]" → type: "bug", priority: "high"
|
|
397
|
-
- "[ ] Task" → completed: false
|
|
398
|
-
- "[x] Done" → completed: true
|
|
399
|
-
|
|
400
|
-
VERSION - Extract number:
|
|
401
|
-
- "(v0.11.6)" or "v0.11.6" → version: "0.11.6"
|
|
402
|
-
- "Security Hardening v0.2.0" → name: "Security Hardening", version: "0.2.0"
|
|
403
|
-
|
|
404
|
-
SECTIONS - Detect from headers:
|
|
405
|
-
- "## Active Tasks" → section: "active"
|
|
406
|
-
- "## Previously Active" → section: "previously_active"
|
|
407
|
-
- "### Pain Points:" or "### Riesgos Críticos" → painPoints[] or risks[]
|
|
408
|
-
- "### Why This Feature?" → why[]
|
|
409
|
-
- "### Technical Notes" → technicalNotes[]
|
|
410
|
-
- "### Stack Definido" → stack: {frontend, backend, payments, ai, deploy}
|
|
411
|
-
- "### Módulos V1" → modules: [{name, description}]
|
|
412
|
-
- "### Roles" → roles: [{name, description}]
|
|
413
|
-
|
|
414
|
-
SOURCE PATTERNS:
|
|
415
|
-
- "**Source**: docs/technical-spec-v1.md, docs/edr-v1.md" → source: "docs/...", sourceFiles: ["docs/technical-spec-v1.md", "docs/edr-v1.md"]
|
|
416
|
-
|
|
417
|
-
STATUS WITH DATE:
|
|
418
|
-
- "**Status**: COMPLETED 2025-11-29" → status: "completed", completedAt: "2025-11-29T00:00:00.000Z"
|
|
419
|
-
- "### Sprint 6 - Reports + Audits (7 tasks, ~25m) - 2025-12-09 ✅" → sprintName, taskCount, duration, completedDate, status: "completed"
|
|
420
|
-
|
|
421
|
-
DESCRIPTION EXTRACTION:
|
|
422
|
-
- Capture ALL narrative text between the title and bullet points as "description"
|
|
423
|
-
- Code blocks (triple backticks) → codeSnippets: ["code string"]
|
|
424
|
-
- "CRITICAL: Multi-tenant isolation..." → description: "CRITICAL: Multi-tenant isolation..."
|
|
425
|
-
|
|
426
|
-
## Instructions:
|
|
427
|
-
- Return ONLY the JSON object, nothing else
|
|
428
|
-
- Generate unique IDs: task_xxxxxxxx, feat_xxxxxxxx, idea_xxxxxxxx, ship_xxxxxxxx
|
|
429
|
-
- ALL dates must be ISO-8601: YYYY-MM-DDTHH:mm:ss.sssZ
|
|
430
|
-
- Set lastUpdated to: "${new Date().toISOString()}"
|
|
431
|
-
- Preserve ALL rich data from MD - don't lose any information
|
|
432
|
-
- If a field is optional and no data exists, omit it completely`
|
|
433
|
-
})
|
|
434
|
-
|
|
435
|
-
// Parse the JSON response
|
|
436
|
-
const jsonMatch = text.match(/\{[\s\S]*\}/)
|
|
437
|
-
if (!jsonMatch) {
|
|
438
|
-
throw new Error('No valid JSON found in response')
|
|
439
|
-
}
|
|
440
|
-
const parsedData = JSON.parse(jsonMatch[0])
|
|
441
|
-
|
|
442
|
-
await writeJsonFile(projectId, file.jsonPath, parsedData)
|
|
443
|
-
results.push({ file: file.name, success: true })
|
|
444
|
-
} catch (error) {
|
|
445
|
-
results.push({
|
|
446
|
-
file: file.name,
|
|
447
|
-
success: false,
|
|
448
|
-
error: error instanceof Error ? error.message : 'Unknown error'
|
|
449
|
-
})
|
|
450
|
-
}
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
// Copy project.json to data/ if exists
|
|
454
|
-
try {
|
|
455
|
-
const projectJson = await readMdFile(projectId, 'project.json')
|
|
456
|
-
if (projectJson) {
|
|
457
|
-
await writeJsonFile(projectId, 'data/project.json', JSON.parse(projectJson))
|
|
458
|
-
results.push({ file: 'project', success: true })
|
|
459
|
-
}
|
|
460
|
-
} catch {
|
|
461
|
-
// project.json doesn't exist or invalid, skip
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
const allSuccess = results.filter(r => !r.error?.includes('skipped')).every(r => r.success)
|
|
465
|
-
|
|
466
|
-
// Delete legacy files only if all migrations succeeded
|
|
467
|
-
let deletedFiles: string[] = []
|
|
468
|
-
if (allSuccess && deleteLegacy) {
|
|
469
|
-
deletedFiles = await deleteLegacyFiles(projectId)
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
// NOTE: View generation removed from migration to prevent Bun crashes
|
|
473
|
-
// Views are generated on-demand by the view-generator when needed
|
|
474
|
-
// The JSON files in data/ are the source of truth now
|
|
475
|
-
|
|
476
|
-
return { success: allSuccess, results, deletedFiles, viewsGenerated: false }
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
export type ProjectInfo = {
|
|
480
|
-
id: string
|
|
481
|
-
name: string
|
|
482
|
-
needsMigration: boolean
|
|
483
|
-
hasMdFiles: boolean
|
|
484
|
-
hasDataDir: boolean
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
export async function getProjectsToMigrate(): Promise<ProjectInfo[]> {
|
|
488
|
-
try {
|
|
489
|
-
const dirs = await fs.readdir(GLOBAL_STORAGE)
|
|
490
|
-
const validProjects: ProjectInfo[] = []
|
|
491
|
-
|
|
492
|
-
for (const dir of dirs) {
|
|
493
|
-
// Skip hidden directories
|
|
494
|
-
if (dir.startsWith('.')) continue
|
|
495
|
-
|
|
496
|
-
const projectPath = join(GLOBAL_STORAGE, dir)
|
|
497
|
-
|
|
498
|
-
// Must have CLAUDE.md or project.json to be a valid project
|
|
499
|
-
let claudeMd: string | null = null
|
|
500
|
-
try {
|
|
501
|
-
claudeMd = await fs.readFile(join(projectPath, 'CLAUDE.md'), 'utf-8')
|
|
502
|
-
} catch {
|
|
503
|
-
// No CLAUDE.md
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
// Check for project.json in root or data/
|
|
507
|
-
let projectJson: Record<string, unknown> | null = null
|
|
508
|
-
try {
|
|
509
|
-
projectJson = JSON.parse(await fs.readFile(join(projectPath, 'project.json'), 'utf-8'))
|
|
510
|
-
} catch {
|
|
511
|
-
try {
|
|
512
|
-
projectJson = JSON.parse(await fs.readFile(join(projectPath, 'data', 'project.json'), 'utf-8'))
|
|
513
|
-
} catch {
|
|
514
|
-
// No project.json
|
|
515
|
-
}
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
// Must have at least one identifier
|
|
519
|
-
if (!claudeMd && !projectJson) continue
|
|
520
|
-
|
|
521
|
-
// Check if project has legacy MD files to migrate
|
|
522
|
-
let hasMdFiles = false
|
|
523
|
-
for (const file of MIGRATION_FILES) {
|
|
524
|
-
for (const mdPath of file.mdPaths) {
|
|
525
|
-
if (mdPath.endsWith('.md')) {
|
|
526
|
-
try {
|
|
527
|
-
const content = await fs.readFile(join(projectPath, mdPath), 'utf-8')
|
|
528
|
-
if (content && content.trim().length > 20) {
|
|
529
|
-
hasMdFiles = true
|
|
530
|
-
break
|
|
531
|
-
}
|
|
532
|
-
} catch {
|
|
533
|
-
// File doesn't exist
|
|
534
|
-
}
|
|
535
|
-
}
|
|
536
|
-
}
|
|
537
|
-
if (hasMdFiles) break
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
// Check if data/ directory exists with JSON files
|
|
541
|
-
let hasDataDir = false
|
|
542
|
-
try {
|
|
543
|
-
const dataFiles = await fs.readdir(join(projectPath, 'data'))
|
|
544
|
-
hasDataDir = dataFiles.some(f => f.endsWith('.json'))
|
|
545
|
-
} catch {
|
|
546
|
-
// data/ doesn't exist
|
|
547
|
-
}
|
|
548
|
-
|
|
549
|
-
// Project needs migration if it has MD files but no data/ or incomplete data/
|
|
550
|
-
const needsMigration = hasMdFiles
|
|
551
|
-
|
|
552
|
-
// Get project name
|
|
553
|
-
let name = dir
|
|
554
|
-
if (projectJson && typeof projectJson.name === 'string') {
|
|
555
|
-
name = projectJson.name
|
|
556
|
-
} else if (claudeMd) {
|
|
557
|
-
const match = claudeMd.match(/# (.+) - Project Context/)
|
|
558
|
-
if (match) name = match[1]
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
validProjects.push({
|
|
562
|
-
id: dir,
|
|
563
|
-
name,
|
|
564
|
-
needsMigration,
|
|
565
|
-
hasMdFiles,
|
|
566
|
-
hasDataDir
|
|
567
|
-
})
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
// Sort: projects needing migration first, then by name
|
|
571
|
-
return validProjects.sort((a, b) => {
|
|
572
|
-
if (a.needsMigration !== b.needsMigration) {
|
|
573
|
-
return a.needsMigration ? -1 : 1
|
|
574
|
-
}
|
|
575
|
-
return a.name.localeCompare(b.name)
|
|
576
|
-
})
|
|
577
|
-
} catch {
|
|
578
|
-
return []
|
|
579
|
-
}
|
|
580
|
-
}
|