prjct-cli 0.4.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 (71) hide show
  1. package/CHANGELOG.md +312 -0
  2. package/CLAUDE.md +300 -0
  3. package/LICENSE +21 -0
  4. package/README.md +424 -0
  5. package/bin/prjct +214 -0
  6. package/core/agent-detector.js +249 -0
  7. package/core/agents/claude-agent.js +250 -0
  8. package/core/agents/codex-agent.js +256 -0
  9. package/core/agents/terminal-agent.js +465 -0
  10. package/core/analyzer.js +596 -0
  11. package/core/animations-simple.js +240 -0
  12. package/core/animations.js +277 -0
  13. package/core/author-detector.js +218 -0
  14. package/core/capability-installer.js +190 -0
  15. package/core/command-installer.js +775 -0
  16. package/core/commands.js +2050 -0
  17. package/core/config-manager.js +335 -0
  18. package/core/migrator.js +784 -0
  19. package/core/path-manager.js +324 -0
  20. package/core/project-capabilities.js +144 -0
  21. package/core/session-manager.js +439 -0
  22. package/core/version.js +107 -0
  23. package/core/workflow-engine.js +213 -0
  24. package/core/workflow-prompts.js +192 -0
  25. package/core/workflow-rules.js +147 -0
  26. package/package.json +80 -0
  27. package/scripts/install.sh +433 -0
  28. package/scripts/verify-installation.sh +158 -0
  29. package/templates/agents/AGENTS.md +164 -0
  30. package/templates/commands/analyze.md +125 -0
  31. package/templates/commands/cleanup.md +102 -0
  32. package/templates/commands/context.md +105 -0
  33. package/templates/commands/design.md +113 -0
  34. package/templates/commands/done.md +44 -0
  35. package/templates/commands/fix.md +87 -0
  36. package/templates/commands/git.md +79 -0
  37. package/templates/commands/help.md +72 -0
  38. package/templates/commands/idea.md +50 -0
  39. package/templates/commands/init.md +237 -0
  40. package/templates/commands/next.md +74 -0
  41. package/templates/commands/now.md +35 -0
  42. package/templates/commands/progress.md +92 -0
  43. package/templates/commands/recap.md +86 -0
  44. package/templates/commands/roadmap.md +107 -0
  45. package/templates/commands/ship.md +41 -0
  46. package/templates/commands/stuck.md +48 -0
  47. package/templates/commands/task.md +97 -0
  48. package/templates/commands/test.md +94 -0
  49. package/templates/commands/workflow.md +224 -0
  50. package/templates/examples/natural-language-examples.md +320 -0
  51. package/templates/mcp-config.json +8 -0
  52. package/templates/workflows/analyze.md +159 -0
  53. package/templates/workflows/cleanup.md +73 -0
  54. package/templates/workflows/context.md +72 -0
  55. package/templates/workflows/design.md +88 -0
  56. package/templates/workflows/done.md +20 -0
  57. package/templates/workflows/fix.md +201 -0
  58. package/templates/workflows/git.md +192 -0
  59. package/templates/workflows/help.md +13 -0
  60. package/templates/workflows/idea.md +22 -0
  61. package/templates/workflows/init.md +80 -0
  62. package/templates/workflows/natural-language-handler.md +183 -0
  63. package/templates/workflows/next.md +44 -0
  64. package/templates/workflows/now.md +19 -0
  65. package/templates/workflows/progress.md +113 -0
  66. package/templates/workflows/recap.md +66 -0
  67. package/templates/workflows/roadmap.md +95 -0
  68. package/templates/workflows/ship.md +18 -0
  69. package/templates/workflows/stuck.md +25 -0
  70. package/templates/workflows/task.md +109 -0
  71. package/templates/workflows/test.md +243 -0
@@ -0,0 +1,784 @@
1
+ const fs = require('fs').promises
2
+ const path = require('path')
3
+ const pathManager = require('./path-manager')
4
+ const configManager = require('./config-manager')
5
+ const authorDetector = require('./author-detector')
6
+
7
+ /**
8
+ * Migrator - Handles migrations between versions
9
+ *
10
+ * Migration process:
11
+ * 1. Detect legacy .prjct directory (v0.1.0 β†’ v0.2.x)
12
+ * 2. Detect author information
13
+ * 3. Create prjct.config.json
14
+ * 4. Create global directory structure
15
+ * 5. Copy all files to global location
16
+ * 6. Move authors/version/created/lastSync to global config (v0.2.x β†’ v0.3.0)
17
+ * 7. Validate migration
18
+ * 8. Optionally remove local .prjct
19
+ *
20
+ * @version 0.3.0
21
+ */
22
+ class Migrator {
23
+ /**
24
+ * Check if a project needs migration
25
+ *
26
+ * @param {string} projectPath - Path to the project
27
+ * @returns {Promise<boolean>} - True if migration needed
28
+ */
29
+ async needsMigration(projectPath) {
30
+ const structureMigration = await configManager.needsMigration(projectPath)
31
+ if (structureMigration) return true
32
+
33
+ const config = await configManager.readConfig(projectPath)
34
+ if (config && config.version && config.version.startsWith('0.2.')) {
35
+ return true
36
+ }
37
+
38
+ return false
39
+ }
40
+
41
+ /**
42
+ * Migrate config from 0.2.x to 0.3.0 (move authors to global config)
43
+ *
44
+ * @param {string} projectPath - Path to the project
45
+ * @returns {Promise<Object>} - Migration result
46
+ */
47
+ async migrateConfigTo030(projectPath) {
48
+ const result = {
49
+ success: false,
50
+ message: '',
51
+ oldVersion: null,
52
+ newVersion: '0.3.0',
53
+ }
54
+
55
+ try {
56
+ const localConfig = await configManager.readConfig(projectPath)
57
+ if (!localConfig) {
58
+ result.message = 'No config found'
59
+ return result
60
+ }
61
+
62
+ result.oldVersion = localConfig.version
63
+ const projectId = localConfig.projectId
64
+
65
+ const globalConfig = await configManager.readGlobalConfig(projectId)
66
+ if (globalConfig && globalConfig.authors && globalConfig.authors.length > 0) {
67
+ const needsCleanup = localConfig.authors || localConfig.author ||
68
+ localConfig.version || localConfig.created || localConfig.lastSync
69
+
70
+ if (needsCleanup) {
71
+ delete localConfig.authors
72
+ delete localConfig.author
73
+ delete localConfig.version
74
+ delete localConfig.created
75
+ delete localConfig.lastSync
76
+ await configManager.writeConfig(projectPath, localConfig)
77
+ }
78
+ result.success = true
79
+ result.message = 'Authors already in global config, cleaned up local config'
80
+ return result
81
+ }
82
+
83
+ let authors = []
84
+ const now = new Date().toISOString()
85
+
86
+ if (localConfig.authors && Array.isArray(localConfig.authors)) {
87
+ authors = localConfig.authors
88
+ } else if (localConfig.author) {
89
+ authors = [
90
+ {
91
+ name: localConfig.author.name || 'Unknown',
92
+ email: localConfig.author.email || '',
93
+ github: localConfig.author.github || '',
94
+ firstContribution: localConfig.created || now,
95
+ lastActivity: localConfig.lastSync || now,
96
+ },
97
+ ]
98
+ } else {
99
+ authors = [
100
+ {
101
+ name: 'Unknown',
102
+ email: '',
103
+ github: '',
104
+ firstContribution: now,
105
+ lastActivity: now,
106
+ },
107
+ ]
108
+ }
109
+
110
+ const newGlobalConfig = {
111
+ projectId,
112
+ authors,
113
+ version: '0.3.0',
114
+ created: localConfig.created || now,
115
+ lastSync: now,
116
+ }
117
+ await configManager.writeGlobalConfig(projectId, newGlobalConfig)
118
+
119
+ delete localConfig.authors
120
+ delete localConfig.author
121
+ delete localConfig.version
122
+ delete localConfig.created
123
+ delete localConfig.lastSync
124
+ await configManager.writeConfig(projectPath, localConfig)
125
+
126
+ result.success = true
127
+ result.message = `Migrated ${authors.length} author(s) to global config`
128
+ return result
129
+ } catch (error) {
130
+ result.message = `Migration failed: ${error.message}`
131
+ return result
132
+ }
133
+ }
134
+
135
+ /**
136
+ * Copy a directory recursively
137
+ *
138
+ * @param {string} source - Source directory
139
+ * @param {string} destination - Destination directory
140
+ * @returns {Promise<number>} - Number of files copied
141
+ * @private
142
+ */
143
+ async copyDirectory(source, destination) {
144
+ let fileCount = 0
145
+
146
+ await fs.mkdir(destination, { recursive: true })
147
+
148
+ const entries = await fs.readdir(source, { withFileTypes: true })
149
+
150
+ for (const entry of entries) {
151
+ const sourcePath = path.join(source, entry.name)
152
+ const destPath = path.join(destination, entry.name)
153
+
154
+ if (entry.isDirectory()) {
155
+ fileCount += await this.copyDirectory(sourcePath, destPath)
156
+ } else {
157
+ await fs.copyFile(sourcePath, destPath)
158
+ fileCount++
159
+ }
160
+ }
161
+
162
+ return fileCount
163
+ }
164
+
165
+ /**
166
+ * Map legacy flat structure to new layered structure
167
+ *
168
+ * @param {string} filename - Name of the file
169
+ * @returns {Object} - {layer, filename}
170
+ * @private
171
+ */
172
+ mapLegacyFile(filename) {
173
+ if (filename === 'now.md' || filename === 'next.md' || filename === 'context.md') {
174
+ return { layer: 'core', filename }
175
+ }
176
+
177
+ if (filename === 'shipped.md' || filename === 'metrics.md') {
178
+ return { layer: 'progress', filename }
179
+ }
180
+
181
+ if (filename === 'ideas.md' || filename === 'roadmap.md') {
182
+ return { layer: 'planning', filename }
183
+ }
184
+
185
+ if (filename === 'memory.jsonl' || filename === 'context.jsonl' || filename === 'decisions.jsonl') {
186
+ return { layer: 'memory', filename }
187
+ }
188
+
189
+ if (filename === 'repo-summary.md') {
190
+ return { layer: 'analysis', filename }
191
+ }
192
+
193
+ return { layer: '.', filename }
194
+ }
195
+
196
+ /**
197
+ * Migrate files from legacy structure to new layered structure
198
+ *
199
+ * @param {string} legacyPath - Path to legacy .prjct directory
200
+ * @param {string} globalPath - Path to new global project directory
201
+ * @returns {Promise<{fileCount: number, layerCounts: Object}>}
202
+ * @private
203
+ */
204
+ async migrateFiles(legacyPath, globalPath) {
205
+ let fileCount = 0
206
+ const layerCounts = {
207
+ core: 0,
208
+ progress: 0,
209
+ planning: 0,
210
+ analysis: 0,
211
+ memory: 0,
212
+ other: 0,
213
+ }
214
+
215
+ const validLayers = ['core', 'progress', 'planning', 'analysis', 'memory', 'sessions']
216
+ const entries = await fs.readdir(legacyPath, { withFileTypes: true })
217
+
218
+ for (const entry of entries) {
219
+ const sourcePath = path.join(legacyPath, entry.name)
220
+
221
+ if (entry.name === 'prjct.config.json' || entry.name.endsWith('.old')) {
222
+ continue
223
+ }
224
+
225
+ if (entry.isDirectory()) {
226
+ if (validLayers.includes(entry.name)) {
227
+ const destPath = path.join(globalPath, entry.name)
228
+ const count = await this.copyDirectory(sourcePath, destPath)
229
+ fileCount += count
230
+ if (Object.prototype.hasOwnProperty.call(layerCounts, entry.name)) {
231
+ layerCounts[entry.name] += count
232
+ } else {
233
+ layerCounts.other += count
234
+ }
235
+ } else {
236
+ const destPath = path.join(globalPath, 'planning', entry.name)
237
+ const count = await this.copyDirectory(sourcePath, destPath)
238
+ fileCount += count
239
+ layerCounts.planning += count
240
+ }
241
+ } else {
242
+ const mapping = this.mapLegacyFile(entry.name)
243
+ const destPath = path.join(globalPath, mapping.layer, mapping.filename)
244
+
245
+ await fs.mkdir(path.dirname(destPath), { recursive: true })
246
+
247
+ await fs.copyFile(sourcePath, destPath)
248
+ fileCount++
249
+
250
+ if (mapping.layer === '.') {
251
+ layerCounts.other++
252
+ } else {
253
+ layerCounts[mapping.layer] = (layerCounts[mapping.layer] || 0) + 1
254
+ }
255
+ }
256
+ }
257
+
258
+ return { fileCount, layerCounts }
259
+ }
260
+
261
+ /**
262
+ * Validate that migration was successful
263
+ *
264
+ * @param {string} projectId - Project ID
265
+ * @returns {Promise<{valid: boolean, issues: string[]}>}
266
+ * @private
267
+ */
268
+ async validateMigration(projectId) {
269
+ const issues = []
270
+
271
+ const exists = await pathManager.projectExists(projectId)
272
+ if (!exists) {
273
+ issues.push('Global project directory not found')
274
+ return { valid: false, issues }
275
+ }
276
+
277
+ const globalPath = pathManager.getGlobalProjectPath(projectId)
278
+ const requiredLayers = ['core', 'progress', 'planning', 'analysis', 'memory']
279
+
280
+ for (const layer of requiredLayers) {
281
+ try {
282
+ await fs.access(path.join(globalPath, layer))
283
+ } catch {
284
+ issues.push(`Missing layer directory: ${layer}`)
285
+ }
286
+ }
287
+
288
+ try {
289
+ const coreFiles = await fs.readdir(path.join(globalPath, 'core'))
290
+ if (coreFiles.length === 0) {
291
+ issues.push('No files found in core directory')
292
+ }
293
+ } catch {
294
+ issues.push('Cannot read core directory')
295
+ }
296
+
297
+ return {
298
+ valid: issues.length === 0,
299
+ issues,
300
+ }
301
+ }
302
+
303
+ /**
304
+ * Cleanup legacy directories while preserving config
305
+ * Removes: analysis/, core/, memory/, planning/, progress/, sessions/
306
+ * Keeps: prjct.config.json
307
+ *
308
+ * @param {string} projectPath - Path to the project
309
+ * @returns {Promise<void>}
310
+ * @private
311
+ */
312
+ async cleanupLegacyDirectories(projectPath) {
313
+ const legacyPath = pathManager.getLegacyPrjctPath(projectPath)
314
+ const layersToRemove = ['analysis', 'core', 'memory', 'planning', 'progress', 'sessions']
315
+
316
+ for (const layer of layersToRemove) {
317
+ const layerPath = path.join(legacyPath, layer)
318
+ try {
319
+ await fs.rm(layerPath, { recursive: true, force: true })
320
+ } catch {
321
+
322
+ }
323
+ }
324
+ }
325
+
326
+ /**
327
+ * Perform the complete migration process
328
+ *
329
+ * @param {string} projectPath - Path to the project
330
+ * @param {Object} options - Migration options
331
+ * @param {boolean} options.removeLegacy - Remove legacy .prjct after migration completely
332
+ * @param {boolean} options.cleanupLegacy - Remove legacy directories but keep config
333
+ * @param {boolean} options.dryRun - Simulate migration without making changes
334
+ * @returns {Promise<Object>} - Migration result
335
+ */
336
+ async migrate(projectPath, options = {}) {
337
+ const result = {
338
+ success: false,
339
+ projectId: null,
340
+ filescopied: 0,
341
+ layerCounts: {},
342
+ config: null,
343
+ author: null,
344
+ issues: [],
345
+ dryRun: options.dryRun || false,
346
+ }
347
+
348
+ try {
349
+ const config = await configManager.readConfig(projectPath)
350
+ if (config && config.version && config.version.startsWith('0.2.')) {
351
+ const versionMigration = await this.migrateConfigTo030(projectPath)
352
+ result.success = versionMigration.success
353
+ result.projectId = config.projectId
354
+ result.filesCopied = 0
355
+ result.issues = versionMigration.success ? [] : [versionMigration.message]
356
+ return result
357
+ }
358
+
359
+ const needsStructuralMigration = await configManager.needsMigration(projectPath)
360
+ if (!needsStructuralMigration) {
361
+ result.success = false
362
+ result.issues.push('No migration needed - either no legacy structure or already migrated')
363
+ return result
364
+ }
365
+
366
+ result.author = await authorDetector.detect()
367
+
368
+ const projectId = pathManager.generateProjectId(projectPath)
369
+ result.projectId = projectId
370
+
371
+ if (options.dryRun) {
372
+ result.success = true
373
+ result.issues.push('DRY RUN - No changes were made')
374
+ return result
375
+ }
376
+
377
+ await pathManager.ensureProjectStructure(projectId)
378
+
379
+ result.config = await configManager.createConfig(projectPath, result.author)
380
+
381
+ const legacyPath = pathManager.getLegacyPrjctPath(projectPath)
382
+ const globalPath = pathManager.getGlobalProjectPath(projectId)
383
+
384
+ const migrationStats = await this.migrateFiles(legacyPath, globalPath)
385
+ result.filesCopied = migrationStats.fileCount
386
+ result.layerCounts = migrationStats.layerCounts
387
+
388
+ const validation = await this.validateMigration(projectId)
389
+ result.issues = validation.issues
390
+
391
+ if (!validation.valid) {
392
+ result.success = false
393
+ return result
394
+ }
395
+
396
+ if (options.removeLegacy) {
397
+ await fs.rm(legacyPath, { recursive: true, force: true })
398
+ result.legacyRemoved = true
399
+ } else if (options.cleanupLegacy) {
400
+ await this.cleanupLegacyDirectories(projectPath)
401
+ result.legacyCleaned = true
402
+ }
403
+
404
+ result.success = true
405
+ return result
406
+ } catch (error) {
407
+ result.success = false
408
+ result.issues.push(`Migration error: ${error.message}`)
409
+ return result
410
+ }
411
+ }
412
+
413
+ /**
414
+ * Generate a migration report
415
+ *
416
+ * @param {Object} result - Migration result
417
+ * @returns {string} - Formatted report
418
+ */
419
+ generateReport(result) {
420
+ const lines = []
421
+
422
+ lines.push('πŸ“¦ Migration Report')
423
+ lines.push('━━━━━━━━━━━━━━━━━━━━━━━━━━━━')
424
+
425
+ if (result.dryRun) {
426
+ lines.push('⚠️ DRY RUN MODE - No changes were made')
427
+ lines.push('')
428
+ }
429
+
430
+ if (result.success) {
431
+ lines.push('βœ… Migration successful!')
432
+ lines.push('')
433
+ lines.push(`πŸ“‹ Project ID: ${result.projectId}`)
434
+ lines.push(`πŸ‘€ Author: ${authorDetector.formatAuthor(result.author)}`)
435
+ lines.push(`πŸ“ Files migrated: ${result.filesCopied}`)
436
+ lines.push('')
437
+ lines.push('πŸ“‚ Files by layer:')
438
+ for (const [layer, count] of Object.entries(result.layerCounts)) {
439
+ if (count > 0) {
440
+ lines.push(` β€’ ${layer}: ${count} files`)
441
+ }
442
+ }
443
+ lines.push('')
444
+ lines.push(`πŸ“ Data location: ${result.config.dataPath}`)
445
+
446
+ if (result.legacyRemoved) {
447
+ lines.push('')
448
+ lines.push('πŸ—‘οΈ Legacy .prjct directory removed')
449
+ }
450
+ } else {
451
+ lines.push('❌ Migration failed!')
452
+ lines.push('')
453
+ if (result.issues.length > 0) {
454
+ lines.push('Issues:')
455
+ for (const issue of result.issues) {
456
+ lines.push(` β€’ ${issue}`)
457
+ }
458
+ }
459
+ }
460
+
461
+ lines.push('━━━━━━━━━━━━━━━━━━━━━━━━━━━━')
462
+
463
+ return lines.join('\n')
464
+ }
465
+
466
+ /**
467
+ * Check migration status for a project
468
+ *
469
+ * @param {string} projectPath - Path to the project
470
+ * @returns {Promise<Object>} - Status information
471
+ */
472
+ async checkStatus(projectPath) {
473
+ const hasLegacy = await pathManager.hasLegacyStructure(projectPath)
474
+ const hasConfig = await pathManager.hasConfig(projectPath)
475
+ const needsMigration = hasLegacy && !hasConfig
476
+
477
+ let status = 'unknown'
478
+ if (!hasLegacy && !hasConfig) {
479
+ status = 'new' // New project, not initialized
480
+ } else if (!hasLegacy && hasConfig) {
481
+ status = 'migrated' // Already migrated to v0.2.0
482
+ } else if (hasLegacy && !hasConfig) {
483
+ status = 'legacy' // v0.1.0, needs migration
484
+ } else if (hasLegacy && hasConfig) {
485
+ status = 'both' // Has both (migration incomplete or manual setup)
486
+ }
487
+
488
+ return {
489
+ status,
490
+ hasLegacy,
491
+ hasConfig,
492
+ needsMigration,
493
+ version: hasConfig ? '0.2.0' : hasLegacy ? '0.1.0' : 'none',
494
+ }
495
+ }
496
+
497
+ /**
498
+ * Find all projects with .prjct directories on the user's machine
499
+ *
500
+ * @param {Object} options - Search options
501
+ * @param {boolean} options.deepScan - Scan entire home directory (default: true for automatic migration)
502
+ * @returns {Promise<Array<string>>} - Array of project paths
503
+ */
504
+ async findAllProjects(options = {}) {
505
+ const { deepScan = true } = options
506
+ const projectDirs = []
507
+ const os = require('os')
508
+
509
+ let searchPaths = []
510
+ if (deepScan) {
511
+ searchPaths = [os.homedir()]
512
+ } else {
513
+ const commonDirs = ['Projects', 'Documents', 'Developer', 'Code', 'dev', 'workspace', 'repos', 'src']
514
+ searchPaths = commonDirs
515
+ .map(dir => path.join(os.homedir(), dir))
516
+ .filter(dirPath => {
517
+ try {
518
+ fs.accessSync(dirPath)
519
+ return true
520
+ } catch {
521
+ return false
522
+ }
523
+ })
524
+ }
525
+
526
+ const shouldSkip = (dirName) => {
527
+ const skipDirs = [
528
+ 'node_modules',
529
+ '.git',
530
+ '.next',
531
+ 'dist',
532
+ 'build',
533
+ '.cache',
534
+ 'coverage',
535
+ '.vscode',
536
+ '.idea',
537
+ 'vendor',
538
+ '__pycache__',
539
+ ]
540
+ return skipDirs.includes(dirName) || (dirName.startsWith('.') && dirName !== '.prjct')
541
+ }
542
+
543
+ const searchDirectory = async(dirPath, depth = 0) => {
544
+ if (depth > 10) return
545
+
546
+ try {
547
+ const entries = await fs.readdir(dirPath, { withFileTypes: true })
548
+
549
+ if (entries.some(entry => entry.name === '.prjct' && entry.isDirectory())) {
550
+ projectDirs.push(dirPath)
551
+ return // Don't search subdirectories if we found a project
552
+ }
553
+
554
+ for (const entry of entries) {
555
+ if (entry.isDirectory() && !shouldSkip(entry.name)) {
556
+ const subPath = path.join(dirPath, entry.name)
557
+ await searchDirectory(subPath, depth + 1)
558
+ }
559
+ }
560
+ } catch (error) {
561
+
562
+ }
563
+ }
564
+
565
+ for (const searchPath of searchPaths) {
566
+ await searchDirectory(searchPath)
567
+ }
568
+
569
+ return projectDirs
570
+ }
571
+
572
+ /**
573
+ * Migrate all projects with legacy .prjct directories
574
+ *
575
+ * @param {Object} options - Migration options
576
+ * @param {boolean} options.deepScan - Scan entire home directory
577
+ * @param {boolean} options.removeLegacy - Remove legacy .prjct after migration completely
578
+ * @param {boolean} options.cleanupLegacy - Remove legacy directories but keep config
579
+ * @param {boolean} options.dryRun - Simulate migration without making changes
580
+ * @param {boolean} options.interactive - Ask for confirmation before each migration
581
+ * @param {Function} options.onProgress - Callback for progress updates
582
+ * @returns {Promise<Object>} - Migration summary
583
+ */
584
+ async migrateAll(options = {}) {
585
+ const {
586
+ deepScan = false,
587
+ removeLegacy = false,
588
+ cleanupLegacy = false,
589
+ dryRun = false,
590
+ interactive = false,
591
+ onProgress = null,
592
+ } = options
593
+
594
+ const summary = {
595
+ success: false,
596
+ totalFound: 0,
597
+ alreadyMigrated: 0,
598
+ successfullyMigrated: 0,
599
+ failed: 0,
600
+ skipped: 0,
601
+ projects: [],
602
+ errors: [],
603
+ dryRun,
604
+ }
605
+
606
+ try {
607
+ if (onProgress) onProgress({ phase: 'scanning', message: 'Searching for projects...' })
608
+ const projectPaths = await this.findAllProjects({ deepScan })
609
+ summary.totalFound = projectPaths.length
610
+
611
+ if (projectPaths.length === 0) {
612
+ summary.success = true
613
+ return summary
614
+ }
615
+
616
+ for (let i = 0; i < projectPaths.length; i++) {
617
+ const projectPath = projectPaths[i]
618
+ const projectName = path.basename(projectPath)
619
+
620
+ if (onProgress) {
621
+ onProgress({
622
+ phase: 'checking',
623
+ message: `Checking ${projectName} (${i + 1}/${projectPaths.length})`,
624
+ current: i + 1,
625
+ total: projectPaths.length,
626
+ })
627
+ }
628
+
629
+ try {
630
+ const status = await this.checkStatus(projectPath)
631
+
632
+ const projectInfo = {
633
+ path: projectPath,
634
+ name: projectName,
635
+ status: status.status,
636
+ }
637
+
638
+ if (status.status === 'migrated' || status.status === 'new') {
639
+ projectInfo.result = 'skipped'
640
+ projectInfo.reason = status.status === 'migrated' ? 'Already migrated' : 'Not initialized'
641
+ summary.alreadyMigrated++
642
+ } else if (status.needsMigration) {
643
+ if (interactive && onProgress) {
644
+ const shouldMigrate = await onProgress({
645
+ phase: 'confirm',
646
+ message: `Migrate ${projectName}?`,
647
+ projectPath,
648
+ })
649
+ if (!shouldMigrate) {
650
+ projectInfo.result = 'skipped'
651
+ projectInfo.reason = 'User skipped'
652
+ summary.skipped++
653
+ summary.projects.push(projectInfo)
654
+ continue
655
+ }
656
+ }
657
+
658
+ if (onProgress) {
659
+ onProgress({
660
+ phase: 'migrating',
661
+ message: `Migrating ${projectName}...`,
662
+ current: i + 1,
663
+ total: projectPaths.length,
664
+ })
665
+ }
666
+
667
+ const migrationResult = await this.migrate(projectPath, {
668
+ removeLegacy,
669
+ cleanupLegacy,
670
+ dryRun,
671
+ })
672
+
673
+ projectInfo.projectId = migrationResult.projectId
674
+ projectInfo.filesCopied = migrationResult.filesCopied
675
+ projectInfo.layerCounts = migrationResult.layerCounts
676
+
677
+ if (migrationResult.success) {
678
+ projectInfo.result = 'success'
679
+ summary.successfullyMigrated++
680
+ } else {
681
+ projectInfo.result = 'failed'
682
+ projectInfo.errors = migrationResult.issues
683
+ summary.failed++
684
+ summary.errors.push({
685
+ project: projectName,
686
+ path: projectPath,
687
+ issues: migrationResult.issues,
688
+ })
689
+ }
690
+ }
691
+
692
+ summary.projects.push(projectInfo)
693
+ } catch (error) {
694
+ summary.failed++
695
+ summary.errors.push({
696
+ project: projectName,
697
+ path: projectPath,
698
+ issues: [error.message],
699
+ })
700
+ summary.projects.push({
701
+ path: projectPath,
702
+ name: projectName,
703
+ result: 'failed',
704
+ errors: [error.message],
705
+ })
706
+ }
707
+ }
708
+
709
+ summary.success = summary.failed === 0
710
+ return summary
711
+ } catch (error) {
712
+ summary.success = false
713
+ summary.errors.push({
714
+ project: 'global',
715
+ issues: [error.message],
716
+ })
717
+ return summary
718
+ }
719
+ }
720
+
721
+ /**
722
+ * Generate a summary report for migrateAll results
723
+ *
724
+ * @param {Object} summary - Migration summary from migrateAll
725
+ * @returns {string} - Formatted report
726
+ */
727
+ generateMigrationSummary(summary) {
728
+ const lines = []
729
+
730
+ lines.push('πŸ“¦ Global Migration Report')
731
+ lines.push('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━')
732
+
733
+ if (summary.dryRun) {
734
+ lines.push('⚠️ DRY RUN MODE - No changes were made')
735
+ lines.push('')
736
+ }
737
+
738
+ lines.push(`πŸ” Found: ${summary.totalFound} projects`)
739
+ lines.push(`βœ… Successfully migrated: ${summary.successfullyMigrated}`)
740
+ lines.push(`⏭️ Already migrated: ${summary.alreadyMigrated}`)
741
+ if (summary.skipped > 0) {
742
+ lines.push(`⏸️ Skipped: ${summary.skipped}`)
743
+ }
744
+ if (summary.failed > 0) {
745
+ lines.push(`❌ Failed: ${summary.failed}`)
746
+ }
747
+ lines.push('')
748
+
749
+ if (summary.successfullyMigrated > 0) {
750
+ lines.push('βœ… Successfully Migrated:')
751
+ summary.projects
752
+ .filter(p => p.result === 'success')
753
+ .forEach(project => {
754
+ lines.push(` β€’ ${project.name}`)
755
+ lines.push(` Files: ${project.filesCopied} | ID: ${project.projectId}`)
756
+ })
757
+ lines.push('')
758
+ }
759
+
760
+ if (summary.errors.length > 0) {
761
+ lines.push('❌ Errors:')
762
+ summary.errors.forEach(error => {
763
+ lines.push(` β€’ ${error.project}`)
764
+ error.issues.forEach(issue => lines.push(` - ${issue}`))
765
+ })
766
+ lines.push('')
767
+ }
768
+
769
+ if (summary.success && summary.successfullyMigrated > 0) {
770
+ lines.push('πŸŽ‰ All projects migrated successfully!')
771
+ lines.push(`πŸ“ Global data location: ${pathManager.getGlobalBasePath()}`)
772
+ } else if (summary.totalFound === 0) {
773
+ lines.push('ℹ️ No legacy projects found')
774
+ } else if (summary.alreadyMigrated === summary.totalFound) {
775
+ lines.push('ℹ️ All projects already migrated')
776
+ }
777
+
778
+ lines.push('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━')
779
+
780
+ return lines.join('\n')
781
+ }
782
+ }
783
+
784
+ module.exports = new Migrator()