prjct-cli 0.12.2 → 0.13.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 (39) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/CLAUDE.md +18 -6
  3. package/core/data/index.ts +19 -5
  4. package/core/data/md-base-manager.ts +203 -0
  5. package/core/data/md-queue-manager.ts +179 -0
  6. package/core/data/md-state-manager.ts +133 -0
  7. package/core/serializers/index.ts +20 -0
  8. package/core/serializers/queue-serializer.ts +210 -0
  9. package/core/serializers/state-serializer.ts +136 -0
  10. package/core/utils/file-helper.ts +12 -0
  11. package/package.json +1 -1
  12. package/packages/web/app/api/projects/[id]/stats/route.ts +6 -29
  13. package/packages/web/app/page.tsx +1 -6
  14. package/packages/web/app/project/[id]/page.tsx +34 -1
  15. package/packages/web/app/project/[id]/stats/page.tsx +11 -5
  16. package/packages/web/app/settings/page.tsx +2 -221
  17. package/packages/web/components/BlockersCard/BlockersCard.tsx +67 -0
  18. package/packages/web/components/BlockersCard/BlockersCard.types.ts +11 -0
  19. package/packages/web/components/BlockersCard/index.ts +2 -0
  20. package/packages/web/components/CommandButton/CommandButton.tsx +10 -3
  21. package/packages/web/lib/projects.ts +28 -27
  22. package/packages/web/lib/services/projects.server.ts +25 -21
  23. package/packages/web/lib/services/stats.server.ts +355 -57
  24. package/packages/web/package.json +0 -2
  25. package/templates/commands/decision.md +226 -0
  26. package/templates/commands/done.md +100 -68
  27. package/templates/commands/feature.md +102 -103
  28. package/templates/commands/idea.md +41 -38
  29. package/templates/commands/now.md +94 -33
  30. package/templates/commands/pause.md +90 -30
  31. package/templates/commands/ship.md +179 -74
  32. package/templates/commands/sync.md +324 -200
  33. package/packages/web/app/api/migrate/route.ts +0 -46
  34. package/packages/web/app/api/settings/route.ts +0 -97
  35. package/packages/web/app/api/v2/projects/[id]/unified/route.ts +0 -57
  36. package/packages/web/components/MigrationGate/MigrationGate.tsx +0 -304
  37. package/packages/web/components/MigrationGate/index.ts +0 -1
  38. package/packages/web/lib/json-loader.ts +0 -630
  39. 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
- }