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