@wundr.io/cli 1.0.1 → 1.0.2-dev.20260530180455.e1307186

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 (69) hide show
  1. package/bin/wundr.js +13 -5
  2. package/package.json +30 -9
  3. package/src/ai/ai-service.ts +6 -4
  4. package/src/ai/claude-client.ts +6 -2
  5. package/src/ai/conversation-manager.ts +12 -5
  6. package/src/cli.ts +42 -13
  7. package/src/commands/ai.ts +340 -64
  8. package/src/commands/alignment.ts +1212 -0
  9. package/src/commands/analyze-optimized.ts +371 -33
  10. package/src/commands/analyze.ts +8 -6
  11. package/src/commands/batch.ts +166 -26
  12. package/src/commands/chat.ts +20 -10
  13. package/src/commands/claude-init.ts +31 -27
  14. package/src/commands/claude-setup.ts +761 -81
  15. package/src/commands/computer-setup.ts +524 -12
  16. package/src/commands/create-command.ts +3 -3
  17. package/src/commands/create.ts +9 -6
  18. package/src/commands/dashboard.ts +11 -6
  19. package/src/commands/govern.ts +11 -6
  20. package/src/commands/governance.ts +1005 -0
  21. package/src/commands/guardian.ts +887 -0
  22. package/src/commands/init.ts +104 -11
  23. package/src/commands/orchestrator.ts +789 -0
  24. package/src/commands/performance-optimizer.ts +15 -10
  25. package/src/commands/plugins.ts +8 -5
  26. package/src/commands/project-update.ts +1156 -0
  27. package/src/commands/rag.ts +1011 -0
  28. package/src/commands/session.ts +631 -0
  29. package/src/commands/setup.ts +42 -344
  30. package/src/commands/test-init.ts +3 -2
  31. package/src/commands/test.ts +3 -2
  32. package/src/commands/watch.ts +21 -11
  33. package/src/commands/worktree.ts +1057 -0
  34. package/src/context/context-manager.ts +5 -2
  35. package/src/context/session-manager.ts +18 -7
  36. package/src/framework/command-interface.ts +520 -0
  37. package/src/framework/command-registry.ts +942 -0
  38. package/src/framework/completion-exporter.ts +383 -0
  39. package/src/framework/debug-logger.ts +519 -0
  40. package/src/framework/error-handler.ts +867 -0
  41. package/src/framework/help-generator.ts +540 -0
  42. package/src/framework/index.ts +169 -0
  43. package/src/framework/interactive-repl.ts +703 -0
  44. package/src/framework/output-formatter.ts +834 -0
  45. package/src/framework/progress-manager.ts +539 -0
  46. package/src/index.ts +3 -2
  47. package/src/interactive/interactive-mode.ts +14 -7
  48. package/src/lib/conflict-resolution.ts +818 -0
  49. package/src/lib/merge-strategy.ts +550 -0
  50. package/src/lib/safety-mechanisms.ts +451 -0
  51. package/src/lib/state-detection.ts +1030 -0
  52. package/src/nlp/command-mapper.ts +8 -3
  53. package/src/nlp/command-parser.ts +5 -2
  54. package/src/nlp/intent-parser.ts +23 -9
  55. package/src/plugins/plugin-manager.ts +50 -24
  56. package/src/tests/computer-setup-integration.test.ts +46 -15
  57. package/src/types/index.ts +1 -1
  58. package/src/types/modules.d.ts +425 -1
  59. package/src/utils/backup-rollback-manager.ts +19 -14
  60. package/src/utils/claude-config-installer.ts +119 -28
  61. package/src/utils/config-manager.ts +9 -6
  62. package/src/utils/error-handler.ts +3 -1
  63. package/src/utils/logger.ts +35 -12
  64. package/templates/batch/ci-cd.yaml +7 -7
  65. package/test-suites/api/health.spec.ts +20 -23
  66. package/test-suites/helpers/test-config.ts +14 -13
  67. package/test-suites/ui/accessibility.spec.ts +27 -22
  68. package/test-suites/ui/smoke.spec.ts +26 -21
  69. package/src/commands/computer-setup-commands.ts +0 -869
@@ -0,0 +1,1156 @@
1
+ /**
2
+ * Project Update Command
3
+ *
4
+ * Main command for updating wundr projects to new versions.
5
+ * Orchestrates state detection, backup, merge, and conflict resolution.
6
+ */
7
+
8
+ import { existsSync } from 'fs';
9
+ import * as fs from 'fs/promises';
10
+ import * as path from 'path';
11
+
12
+ import chalk from 'chalk';
13
+ import { Command } from 'commander';
14
+ import inquirer from 'inquirer';
15
+ import ora from 'ora';
16
+
17
+ import { createConflictResolver } from '../lib/conflict-resolution';
18
+ import {
19
+ MergeStrategyManager,
20
+ MergeResult,
21
+ threeWayMerge,
22
+ detectFileType,
23
+ } from '../lib/merge-strategy';
24
+ import { createSafetyManager } from '../lib/safety-mechanisms';
25
+ import {
26
+ detectProjectState,
27
+ CustomizationInfo,
28
+ getStateSummary,
29
+ } from '../lib/state-detection';
30
+ import { errorHandler } from '../utils/error-handler';
31
+ import { logger } from '../utils/logger';
32
+
33
+ // Import lib modules
34
+ import type {
35
+ ConflictResolver,
36
+ UpdateConflict,
37
+ ConflictResolutionResult,
38
+ } from '../lib/conflict-resolution';
39
+ import type {
40
+ SafetyManager,
41
+ UpdateBackup,
42
+ UpdateTransaction,
43
+ } from '../lib/safety-mechanisms';
44
+ import type { ProjectState } from '../lib/state-detection';
45
+ import type { PluginManager } from '../plugins/plugin-manager';
46
+ import type { ConfigManager } from '../utils/config-manager';
47
+
48
+ /**
49
+ * Update options from CLI flags
50
+ */
51
+ export interface ProjectUpdateOptions {
52
+ /** Dry run mode - show what would be done */
53
+ dryRun: boolean;
54
+ /** Force update without prompts */
55
+ force: boolean;
56
+ /** Skip backup creation */
57
+ skipBackup: boolean;
58
+ /** Specific components to update */
59
+ components: string[];
60
+ /** Target version to update to */
61
+ version: string | null;
62
+ /** Interactive mode */
63
+ interactive: boolean;
64
+ /** Verbose logging */
65
+ verbose: boolean;
66
+ /** Show diff during update */
67
+ showDiff: boolean;
68
+ /** Auto-resolve conflicts */
69
+ autoResolve: boolean;
70
+ /** Rollback on failure */
71
+ rollbackOnFailure: boolean;
72
+ }
73
+
74
+ /**
75
+ * Default update options
76
+ */
77
+ const DEFAULT_UPDATE_OPTIONS: ProjectUpdateOptions = {
78
+ dryRun: false,
79
+ force: false,
80
+ skipBackup: false,
81
+ components: [],
82
+ version: null,
83
+ interactive: true,
84
+ verbose: false,
85
+ showDiff: true,
86
+ autoResolve: false,
87
+ rollbackOnFailure: true,
88
+ };
89
+
90
+ /**
91
+ * Update result
92
+ */
93
+ export interface UpdateResult {
94
+ /** Whether update was successful */
95
+ success: boolean;
96
+ /** Updated from version */
97
+ fromVersion: string;
98
+ /** Updated to version */
99
+ toVersion: string;
100
+ /** Files updated */
101
+ filesUpdated: string[];
102
+ /** Files with conflicts */
103
+ conflicts: UpdateConflict[];
104
+ /** Backup created */
105
+ backup: UpdateBackup | null;
106
+ /** Errors encountered */
107
+ errors: string[];
108
+ /** Update summary */
109
+ summary: UpdateSummary;
110
+ }
111
+
112
+ /**
113
+ * Update summary
114
+ */
115
+ export interface UpdateSummary {
116
+ /** Components checked */
117
+ componentsChecked: number;
118
+ /** Components updated */
119
+ componentsUpdated: number;
120
+ /** Files checked */
121
+ filesChecked: number;
122
+ /** Files updated */
123
+ filesUpdated: number;
124
+ /** Conflicts resolved */
125
+ conflictsResolved: number;
126
+ /** Time taken in ms */
127
+ timeTaken: number;
128
+ }
129
+
130
+ /**
131
+ * Update log entry
132
+ */
133
+ interface UpdateLogEntry {
134
+ timestamp: string;
135
+ action: string;
136
+ target: string;
137
+ status: 'success' | 'failure' | 'skipped';
138
+ details?: string;
139
+ }
140
+
141
+ /**
142
+ * Component information for updates
143
+ */
144
+ interface UpdateComponent {
145
+ name: string;
146
+ files: string[];
147
+ needsUpdate: boolean;
148
+ }
149
+
150
+ /**
151
+ * Project Update Manager
152
+ */
153
+ export class ProjectUpdateManager {
154
+ private projectRoot: string;
155
+ private options: ProjectUpdateOptions;
156
+ private mergeManager: MergeStrategyManager;
157
+ private safetyManager: SafetyManager;
158
+ private conflictResolver: ConflictResolver;
159
+ private updateLog: UpdateLogEntry[] = [];
160
+ private spinner: ReturnType<typeof ora> | null = null;
161
+
162
+ constructor(
163
+ projectRoot: string,
164
+ options: Partial<ProjectUpdateOptions> = {}
165
+ ) {
166
+ this.projectRoot = projectRoot;
167
+ this.options = { ...DEFAULT_UPDATE_OPTIONS, ...options };
168
+
169
+ // Initialize managers
170
+ this.mergeManager = new MergeStrategyManager({
171
+ autoResolve: this.options.autoResolve,
172
+ preserveComments: true,
173
+ });
174
+ this.safetyManager = createSafetyManager({
175
+ projectRoot,
176
+ skipBackup: this.options.skipBackup,
177
+ dryRun: this.options.dryRun,
178
+ });
179
+ this.conflictResolver = createConflictResolver(projectRoot, {
180
+ interactive: this.options.interactive && !this.options.autoResolve,
181
+ autoResolveLow: true,
182
+ autoResolveMedium: this.options.autoResolve,
183
+ });
184
+ }
185
+
186
+ /**
187
+ * Run the project update
188
+ */
189
+ async run(): Promise<UpdateResult> {
190
+ const startTime = Date.now();
191
+
192
+ logger.info('Starting project update', {
193
+ projectRoot: this.projectRoot,
194
+ dryRun: this.options.dryRun,
195
+ force: this.options.force,
196
+ });
197
+
198
+ this.log('update', 'project', 'success', 'Starting project update');
199
+
200
+ try {
201
+ // Step 1: Detect current state
202
+ this.startSpinner('Detecting project state...');
203
+ const currentState = await detectProjectState(this.projectRoot, {
204
+ latestVersion: this.options.version || undefined,
205
+ });
206
+ this.stopSpinner();
207
+
208
+ const currentVersion = currentState.wundrVersion || '0.0.0';
209
+ this.log('detect', 'state', 'success', `Version: ${currentVersion}`);
210
+
211
+ // Step 2: Check if update is needed
212
+ const needsUpdate =
213
+ currentState.isWundrOutdated ||
214
+ currentState.isPartialInstallation ||
215
+ this.options.force;
216
+
217
+ if (!needsUpdate && !this.options.force) {
218
+ console.log(chalk.green('\nProject is up to date!'));
219
+ return this.createResult(
220
+ true,
221
+ currentVersion,
222
+ currentVersion,
223
+ [],
224
+ [],
225
+ null,
226
+ [],
227
+ startTime
228
+ );
229
+ }
230
+
231
+ // Step 3: Show update plan
232
+ await this.showUpdatePlan(currentState);
233
+
234
+ // Step 4: Confirm update (unless force mode)
235
+ if (!this.options.force && !this.options.dryRun) {
236
+ const confirmed = await this.confirmUpdate(currentState);
237
+ if (!confirmed) {
238
+ console.log(chalk.yellow('\nUpdate cancelled by user.'));
239
+ return this.createResult(
240
+ false,
241
+ currentVersion,
242
+ currentVersion,
243
+ [],
244
+ [],
245
+ null,
246
+ ['Cancelled by user'],
247
+ startTime
248
+ );
249
+ }
250
+ }
251
+
252
+ // Step 5: Create backup
253
+ let backup: UpdateBackup | null = null;
254
+ if (!this.options.skipBackup && !this.options.dryRun) {
255
+ this.startSpinner('Creating backup...');
256
+ const filesToBackup = await this.getFilesToBackup(currentState);
257
+ backup = await this.safetyManager.createBackup(
258
+ filesToBackup,
259
+ 'Pre-update backup',
260
+ currentVersion,
261
+ this.options.version || 'latest'
262
+ );
263
+ this.stopSpinner();
264
+ console.log(chalk.green(`Backup created: ${backup.id}`));
265
+ this.log(
266
+ 'backup',
267
+ backup.path,
268
+ 'success',
269
+ `${backup.files.length} files backed up`
270
+ );
271
+ }
272
+
273
+ // Step 6: Start transaction
274
+ const transaction = this.safetyManager.startTransaction('Project update');
275
+
276
+ try {
277
+ // Step 7: Perform updates
278
+ const components = this.extractComponents(currentState);
279
+ const { filesUpdated, conflicts, errors } = await this.performUpdates(
280
+ components,
281
+ currentState,
282
+ transaction
283
+ );
284
+
285
+ // Step 8: Resolve conflicts
286
+ let resolvedConflicts: ConflictResolutionResult[] = [];
287
+ if (conflicts.length > 0) {
288
+ console.log(chalk.yellow(`\n${conflicts.length} conflict(s) found.`));
289
+ this.conflictResolver.startSession(conflicts);
290
+ resolvedConflicts = await this.conflictResolver.resolveAll();
291
+ }
292
+
293
+ // Step 9: Commit transaction
294
+ if (!this.options.dryRun) {
295
+ const committed = await transaction.commit();
296
+ if (!committed) {
297
+ throw new Error('Failed to commit transaction');
298
+ }
299
+ }
300
+
301
+ // Step 10: Write update log
302
+ await this.writeUpdateLog();
303
+
304
+ // Success
305
+ const toVersion = this.options.version || 'latest';
306
+ console.log(
307
+ chalk.green(`\nProject updated successfully to ${toVersion}!`)
308
+ );
309
+
310
+ return this.createResult(
311
+ true,
312
+ currentVersion,
313
+ toVersion,
314
+ filesUpdated,
315
+ conflicts,
316
+ backup,
317
+ errors,
318
+ startTime
319
+ );
320
+ } catch (error) {
321
+ // Rollback on failure
322
+ if (this.options.rollbackOnFailure && backup) {
323
+ console.log(chalk.yellow('\nRolling back changes...'));
324
+ await transaction.rollback();
325
+ await this.safetyManager.restoreFromBackup(backup);
326
+ console.log(chalk.green('Rollback completed.'));
327
+ }
328
+ throw error;
329
+ }
330
+ } catch (error: any) {
331
+ logger.error('Project update failed', error);
332
+ this.log('update', 'project', 'failure', error.message);
333
+ await this.writeUpdateLog();
334
+
335
+ return this.createResult(
336
+ false,
337
+ '0.0.0',
338
+ '0.0.0',
339
+ [],
340
+ [],
341
+ null,
342
+ [error.message],
343
+ startTime
344
+ );
345
+ }
346
+ }
347
+
348
+ /**
349
+ * Extract components from project state
350
+ */
351
+ private extractComponents(state: ProjectState): UpdateComponent[] {
352
+ const components: UpdateComponent[] = [];
353
+
354
+ // Claude config component
355
+ if (state.claudeConfigPath) {
356
+ components.push({
357
+ name: 'claude-config',
358
+ files: [state.claudeConfigPath],
359
+ needsUpdate: !state.hasClaudeConfig || state.isPartialInstallation,
360
+ });
361
+ }
362
+
363
+ // MCP config component
364
+ if (state.mcpConfigPath) {
365
+ components.push({
366
+ name: 'mcp-config',
367
+ files: [state.mcpConfigPath],
368
+ needsUpdate: !state.hasMCPConfig || state.isPartialInstallation,
369
+ });
370
+ }
371
+
372
+ // Wundr config component
373
+ if (state.wundrConfigPath) {
374
+ components.push({
375
+ name: 'wundr-config',
376
+ files: [state.wundrConfigPath],
377
+ needsUpdate: state.isWundrOutdated || false,
378
+ });
379
+ }
380
+
381
+ // Agent configs
382
+ if (state.agents.hasAgents) {
383
+ components.push({
384
+ name: 'agents',
385
+ files: state.agents.agents.map(a => a.configPath),
386
+ needsUpdate: state.agents.agents.some(a => !a.isValid),
387
+ });
388
+ }
389
+
390
+ // Hook configs
391
+ if (state.hooks.hasHooks) {
392
+ components.push({
393
+ name: 'hooks',
394
+ files: state.hooks.hooks.map(h => h.configPath),
395
+ needsUpdate: false,
396
+ });
397
+ }
398
+
399
+ return components;
400
+ }
401
+
402
+ /**
403
+ * Show update plan to user
404
+ */
405
+ private async showUpdatePlan(state: ProjectState): Promise<void> {
406
+ console.log(chalk.cyan('\n========== Update Plan ==========\n'));
407
+
408
+ console.log(
409
+ chalk.white('Current Version:'),
410
+ chalk.yellow(state.wundrVersion || 'Not installed')
411
+ );
412
+ console.log(
413
+ chalk.white('Target Version:'),
414
+ chalk.green(this.options.version || state.latestWundrVersion || 'latest')
415
+ );
416
+ console.log(
417
+ chalk.white('Health Score:'),
418
+ chalk.yellow(`${state.healthScore}/100`)
419
+ );
420
+
421
+ if (state.recommendations.length > 0) {
422
+ console.log(chalk.white('\nRecommendations:'));
423
+ for (const rec of state.recommendations.slice(0, 5)) {
424
+ console.log(chalk.gray(` - ${rec}`));
425
+ }
426
+ }
427
+
428
+ if (state.customizations.hasCustomizations) {
429
+ console.log(chalk.white('\nDetected Customizations:'));
430
+ for (const file of state.customizations.customizedFiles.slice(0, 5)) {
431
+ console.log(` - ${file}`);
432
+ }
433
+ if (state.customizations.customizedFiles.length > 5) {
434
+ console.log(
435
+ chalk.gray(
436
+ ` ... and ${state.customizations.customizedFiles.length - 5} more`
437
+ )
438
+ );
439
+ }
440
+ }
441
+
442
+ if (state.conflicts.hasConflicts) {
443
+ console.log(chalk.yellow('\nDetected Conflicts:'));
444
+ for (const conflict of state.conflicts.conflicts) {
445
+ const severityColor =
446
+ conflict.severity === 'error'
447
+ ? chalk.red
448
+ : conflict.severity === 'warning'
449
+ ? chalk.yellow
450
+ : chalk.gray;
451
+ console.log(
452
+ severityColor(` [${conflict.severity}] ${conflict.description}`)
453
+ );
454
+ }
455
+ }
456
+
457
+ console.log(chalk.cyan('\n=================================\n'));
458
+
459
+ if (this.options.dryRun) {
460
+ console.log(chalk.yellow('DRY RUN MODE - No changes will be made.\n'));
461
+ }
462
+ }
463
+
464
+ /**
465
+ * Confirm update with user
466
+ */
467
+ private async confirmUpdate(state: ProjectState): Promise<boolean> {
468
+ if (this.options.interactive) {
469
+ const { confirmed } = await inquirer.prompt([
470
+ {
471
+ type: 'confirm',
472
+ name: 'confirmed',
473
+ message: 'Proceed with update?',
474
+ default: true,
475
+ },
476
+ ]);
477
+ return confirmed;
478
+ }
479
+ return true;
480
+ }
481
+
482
+ /**
483
+ * Get files to backup
484
+ */
485
+ private async getFilesToBackup(state: ProjectState): Promise<string[]> {
486
+ const files: string[] = [];
487
+
488
+ // Add config files
489
+ if (state.claudeConfigPath) {
490
+ files.push(state.claudeConfigPath);
491
+ }
492
+ if (state.mcpConfigPath) {
493
+ files.push(state.mcpConfigPath);
494
+ }
495
+ if (state.wundrConfigPath) {
496
+ files.push(state.wundrConfigPath);
497
+ }
498
+
499
+ // Add agent config files
500
+ for (const agent of state.agents.agents) {
501
+ files.push(agent.configPath);
502
+ }
503
+
504
+ // Add hook config files
505
+ for (const hook of state.hooks.hooks) {
506
+ files.push(hook.configPath);
507
+ }
508
+
509
+ // Add customized files
510
+ for (const file of state.customizations.customizedFiles) {
511
+ const fullPath = path.join(state.projectPath, file);
512
+ if (existsSync(fullPath)) {
513
+ files.push(fullPath);
514
+ }
515
+ }
516
+
517
+ return files;
518
+ }
519
+
520
+ /**
521
+ * Perform the actual updates
522
+ */
523
+ private async performUpdates(
524
+ components: UpdateComponent[],
525
+ state: ProjectState,
526
+ transaction: UpdateTransaction
527
+ ): Promise<{
528
+ filesUpdated: string[];
529
+ conflicts: UpdateConflict[];
530
+ errors: string[];
531
+ }> {
532
+ const filesUpdated: string[] = [];
533
+ const conflicts: UpdateConflict[] = [];
534
+ const errors: string[] = [];
535
+
536
+ // Filter components if specified
537
+ let componentsToUpdate = components;
538
+ if (this.options.components.length > 0) {
539
+ componentsToUpdate = components.filter(c =>
540
+ this.options.components.includes(c.name)
541
+ );
542
+ }
543
+
544
+ this.startSpinner(`Updating ${componentsToUpdate.length} component(s)...`);
545
+
546
+ for (const component of componentsToUpdate) {
547
+ if (!component.needsUpdate && !this.options.force) {
548
+ continue;
549
+ }
550
+
551
+ try {
552
+ const result = await this.updateComponent(
553
+ component,
554
+ state,
555
+ transaction
556
+ );
557
+
558
+ filesUpdated.push(...result.updated);
559
+ conflicts.push(...result.conflicts);
560
+
561
+ if (result.errors.length > 0) {
562
+ errors.push(...result.errors);
563
+ }
564
+
565
+ this.log(
566
+ 'update',
567
+ component.name,
568
+ 'success',
569
+ `${result.updated.length} files updated`
570
+ );
571
+ } catch (error: any) {
572
+ errors.push(`Component ${component.name}: ${error.message}`);
573
+ this.log('update', component.name, 'failure', error.message);
574
+ }
575
+ }
576
+
577
+ this.stopSpinner();
578
+
579
+ return { filesUpdated, conflicts, errors };
580
+ }
581
+
582
+ /**
583
+ * Update a single component
584
+ */
585
+ private async updateComponent(
586
+ component: UpdateComponent,
587
+ state: ProjectState,
588
+ transaction: UpdateTransaction
589
+ ): Promise<{
590
+ updated: string[];
591
+ conflicts: UpdateConflict[];
592
+ errors: string[];
593
+ }> {
594
+ const updated: string[] = [];
595
+ const conflicts: UpdateConflict[] = [];
596
+ const errors: string[] = [];
597
+
598
+ for (const filePath of component.files) {
599
+ if (!existsSync(filePath)) {
600
+ continue;
601
+ }
602
+
603
+ try {
604
+ // Record operation
605
+ transaction.recordOperation({
606
+ type: 'update',
607
+ path: filePath,
608
+ backupRef: null,
609
+ });
610
+
611
+ // Get current content
612
+ const currentContent = await fs.readFile(filePath, 'utf-8');
613
+
614
+ // Get base content (original version)
615
+ const baseContent = currentContent; // In real implementation, fetch from registry
616
+
617
+ // Get target content (new version)
618
+ const targetContent = await this.getTargetContent(filePath, component);
619
+
620
+ if (!targetContent) {
621
+ // No target content, skip
622
+ transaction.completeOperation(filePath);
623
+ continue;
624
+ }
625
+
626
+ // Perform merge
627
+ const fileType = detectFileType(filePath);
628
+ const mergeResult = await this.mergeManager.threeWayMerge({
629
+ base: baseContent,
630
+ user: currentContent,
631
+ target: targetContent,
632
+ filePath,
633
+ fileType,
634
+ });
635
+
636
+ if (mergeResult.success && mergeResult.content) {
637
+ if (!this.options.dryRun) {
638
+ await fs.writeFile(filePath, mergeResult.content);
639
+ }
640
+ updated.push(filePath);
641
+ transaction.completeOperation(filePath);
642
+
643
+ if (this.options.verbose) {
644
+ console.log(chalk.green(` Updated: ${filePath}`));
645
+ }
646
+ } else if (mergeResult.conflicts.length > 0) {
647
+ // Create update conflicts
648
+ for (const conflict of mergeResult.conflicts) {
649
+ conflicts.push(
650
+ this.conflictResolver.createUpdateConflict(conflict, filePath)
651
+ );
652
+ }
653
+ }
654
+ } catch (error: any) {
655
+ errors.push(`File ${filePath}: ${error.message}`);
656
+ transaction.failOperation(filePath, error.message);
657
+ }
658
+ }
659
+
660
+ return { updated, conflicts, errors };
661
+ }
662
+
663
+ /**
664
+ * Get target content (new version)
665
+ */
666
+ private async getTargetContent(
667
+ filePath: string,
668
+ component: UpdateComponent
669
+ ): Promise<string | null> {
670
+ // In real implementation, would fetch from wundr registry
671
+ // For now, return null (no update available)
672
+ return null;
673
+ }
674
+
675
+ /**
676
+ * Create result object
677
+ */
678
+ private createResult(
679
+ success: boolean,
680
+ fromVersion: string,
681
+ toVersion: string,
682
+ filesUpdated: string[],
683
+ conflicts: UpdateConflict[],
684
+ backup: UpdateBackup | null,
685
+ errors: string[],
686
+ startTime: number
687
+ ): UpdateResult {
688
+ return {
689
+ success,
690
+ fromVersion,
691
+ toVersion,
692
+ filesUpdated,
693
+ conflicts,
694
+ backup,
695
+ errors,
696
+ summary: {
697
+ componentsChecked: 0,
698
+ componentsUpdated: 0,
699
+ filesChecked: 0,
700
+ filesUpdated: filesUpdated.length,
701
+ conflictsResolved: 0,
702
+ timeTaken: Date.now() - startTime,
703
+ },
704
+ };
705
+ }
706
+
707
+ /**
708
+ * Log an update action
709
+ */
710
+ private log(
711
+ action: string,
712
+ target: string,
713
+ status: 'success' | 'failure' | 'skipped',
714
+ details?: string
715
+ ): void {
716
+ this.updateLog.push({
717
+ timestamp: new Date().toISOString(),
718
+ action,
719
+ target,
720
+ status,
721
+ details,
722
+ });
723
+ }
724
+
725
+ /**
726
+ * Write update log to file
727
+ */
728
+ private async writeUpdateLog(): Promise<void> {
729
+ const logPath = path.join(this.projectRoot, '.wundr-update.log');
730
+
731
+ try {
732
+ const content = this.updateLog
733
+ .map(
734
+ entry =>
735
+ `[${entry.timestamp}] ${entry.action.toUpperCase()} ${entry.target} - ${entry.status}${entry.details ? `: ${entry.details}` : ''}`
736
+ )
737
+ .join('\n');
738
+
739
+ await fs.writeFile(logPath, content);
740
+ } catch (error) {
741
+ logger.warn('Failed to write update log', error);
742
+ }
743
+ }
744
+
745
+ /**
746
+ * Start spinner
747
+ */
748
+ private startSpinner(text: string): void {
749
+ if (this.options.verbose || !this.options.interactive) {
750
+ console.log(text);
751
+ } else {
752
+ this.spinner = ora(text).start();
753
+ }
754
+ }
755
+
756
+ /**
757
+ * Stop spinner
758
+ */
759
+ private stopSpinner(success: boolean = true): void {
760
+ if (this.spinner) {
761
+ if (success) {
762
+ this.spinner.succeed();
763
+ } else {
764
+ this.spinner.fail();
765
+ }
766
+ this.spinner = null;
767
+ }
768
+ }
769
+ }
770
+
771
+ /**
772
+ * Project Update Commands Class
773
+ */
774
+ export class ProjectUpdateCommands {
775
+ constructor(
776
+ private program: Command,
777
+ private configManager: ConfigManager,
778
+ private pluginManager: PluginManager
779
+ ) {
780
+ this.registerCommands();
781
+ }
782
+
783
+ private registerCommands(): void {
784
+ const updateCmd = this.program
785
+ .command('update')
786
+ .alias('upgrade')
787
+ .description('Update wundr project to a new version');
788
+
789
+ // Main update command
790
+ updateCmd
791
+ .command('project')
792
+ .description('Update the entire project')
793
+ .option(
794
+ '--dry-run',
795
+ 'Show what would be done without making changes',
796
+ false
797
+ )
798
+ .option('-f, --force', 'Force update without prompts', false)
799
+ .option('--skip-backup', 'Skip creating backup before update', false)
800
+ .option(
801
+ '-c, --components <names>',
802
+ 'Specific components to update (comma-separated)',
803
+ ''
804
+ )
805
+ .option('-v, --version <version>', 'Target version to update to')
806
+ .option('--no-interactive', 'Disable interactive mode')
807
+ .option('--verbose', 'Enable verbose output', false)
808
+ .option('--show-diff', 'Show differences during update', true)
809
+ .option('--auto-resolve', 'Automatically resolve conflicts', false)
810
+ .option('--no-rollback', 'Disable rollback on failure')
811
+ .action(async options => {
812
+ await this.updateProject(options);
813
+ });
814
+
815
+ // Check for updates
816
+ updateCmd
817
+ .command('check')
818
+ .description('Check if updates are available')
819
+ .option('--verbose', 'Show detailed information')
820
+ .action(async options => {
821
+ await this.checkUpdates(options);
822
+ });
823
+
824
+ // Show update history
825
+ updateCmd
826
+ .command('history')
827
+ .description('Show update history')
828
+ .option('-n, --limit <number>', 'Number of entries to show', '10')
829
+ .action(async options => {
830
+ await this.showHistory(options);
831
+ });
832
+
833
+ // Rollback to previous state
834
+ updateCmd
835
+ .command('rollback [backupId]')
836
+ .description('Rollback to a previous state')
837
+ .option('--list', 'List available backups')
838
+ .action(async (backupId, options) => {
839
+ await this.rollback(backupId, options);
840
+ });
841
+
842
+ // Clean up old backups
843
+ updateCmd
844
+ .command('cleanup')
845
+ .description('Clean up old backups')
846
+ .option('-k, --keep <number>', 'Number of backups to keep', '5')
847
+ .action(async options => {
848
+ await this.cleanup(options);
849
+ });
850
+ }
851
+
852
+ /**
853
+ * Update project
854
+ */
855
+ private async updateProject(options: any): Promise<void> {
856
+ try {
857
+ const updateOptions: Partial<ProjectUpdateOptions> = {
858
+ dryRun: options.dryRun,
859
+ force: options.force,
860
+ skipBackup: options.skipBackup,
861
+ components: options.components
862
+ ? options.components.split(',').map((c: string) => c.trim())
863
+ : [],
864
+ version: options.version || null,
865
+ interactive: options.interactive !== false,
866
+ verbose: options.verbose,
867
+ showDiff: options.showDiff,
868
+ autoResolve: options.autoResolve,
869
+ rollbackOnFailure: options.rollback !== false,
870
+ };
871
+
872
+ const manager = new ProjectUpdateManager(process.cwd(), updateOptions);
873
+ const result = await manager.run();
874
+
875
+ if (!result.success) {
876
+ process.exit(1);
877
+ }
878
+ } catch (error) {
879
+ throw errorHandler.createError(
880
+ 'WUNDR_UPDATE_FAILED',
881
+ 'Project update failed',
882
+ { options },
883
+ true
884
+ );
885
+ }
886
+ }
887
+
888
+ /**
889
+ * Check for updates
890
+ */
891
+ private async checkUpdates(options: any): Promise<void> {
892
+ const spinner = ora('Checking for updates...').start();
893
+
894
+ try {
895
+ const state = await detectProjectState();
896
+
897
+ spinner.succeed('Update check complete');
898
+
899
+ console.log(chalk.cyan('\n========== Update Status ==========\n'));
900
+ console.log(
901
+ chalk.white('Current Version:'),
902
+ chalk.yellow(state.wundrVersion || 'Not installed')
903
+ );
904
+ console.log(
905
+ chalk.white('Health Score:'),
906
+ chalk.yellow(`${state.healthScore}/100`)
907
+ );
908
+ console.log(
909
+ chalk.white('Needs Update:'),
910
+ state.isWundrOutdated ? chalk.red('Yes') : chalk.green('No')
911
+ );
912
+
913
+ if (state.recommendations.length > 0) {
914
+ console.log(chalk.white('\nRecommendations:'));
915
+ for (const rec of state.recommendations) {
916
+ console.log(chalk.gray(` - ${rec}`));
917
+ }
918
+
919
+ console.log(
920
+ chalk.cyan('\nRun `wundr update project` to apply updates.')
921
+ );
922
+ }
923
+
924
+ console.log(chalk.cyan('\n====================================\n'));
925
+ } catch (error) {
926
+ spinner.fail('Update check failed');
927
+ throw error;
928
+ }
929
+ }
930
+
931
+ /**
932
+ * Show update history
933
+ */
934
+ private async showHistory(options: any): Promise<void> {
935
+ const logPath = path.join(process.cwd(), '.wundr-update.log');
936
+
937
+ if (!existsSync(logPath)) {
938
+ console.log(chalk.yellow('No update history found.'));
939
+ return;
940
+ }
941
+
942
+ const content = await fs.readFile(logPath, 'utf-8');
943
+ const lines = content.split('\n').slice(0, parseInt(options.limit, 10));
944
+
945
+ console.log(chalk.cyan('\n========== Update History ==========\n'));
946
+ for (const line of lines) {
947
+ if (line.includes('success')) {
948
+ console.log(chalk.green(line));
949
+ } else if (line.includes('failure')) {
950
+ console.log(chalk.red(line));
951
+ } else {
952
+ console.log(chalk.gray(line));
953
+ }
954
+ }
955
+ console.log(chalk.cyan('\n====================================\n'));
956
+ }
957
+
958
+ /**
959
+ * Rollback to previous state
960
+ */
961
+ private async rollback(
962
+ backupId: string | undefined,
963
+ options: any
964
+ ): Promise<void> {
965
+ const safetyManager = createSafetyManager({ projectRoot: process.cwd() });
966
+
967
+ if (options.list) {
968
+ const backups = await safetyManager.listBackups();
969
+
970
+ if (backups.length === 0) {
971
+ console.log(chalk.yellow('No backups available.'));
972
+ return;
973
+ }
974
+
975
+ console.log(chalk.cyan('\n========== Available Backups ==========\n'));
976
+ for (const backup of backups) {
977
+ console.log(
978
+ ` ${chalk.white(backup.id)} - ${chalk.gray(new Date(backup.timestamp).toLocaleString())} ` +
979
+ `(${backup.files.length} files)`
980
+ );
981
+ }
982
+ console.log(chalk.cyan('\n=======================================\n'));
983
+ return;
984
+ }
985
+
986
+ let backup: UpdateBackup | null;
987
+
988
+ if (backupId) {
989
+ backup = await safetyManager
990
+ .listBackups()
991
+ .then(backups => backups.find(b => b.id === backupId) || null);
992
+ } else {
993
+ backup = await safetyManager.getLatestBackup();
994
+ }
995
+
996
+ if (!backup) {
997
+ console.log(chalk.red('Backup not found.'));
998
+ return;
999
+ }
1000
+
1001
+ const { confirmed } = await inquirer.prompt([
1002
+ {
1003
+ type: 'confirm',
1004
+ name: 'confirmed',
1005
+ message: `Rollback to ${backup.id}?`,
1006
+ default: false,
1007
+ },
1008
+ ]);
1009
+
1010
+ if (!confirmed) {
1011
+ console.log(chalk.yellow('Rollback cancelled.'));
1012
+ return;
1013
+ }
1014
+
1015
+ const spinner = ora('Rolling back...').start();
1016
+ const success = await safetyManager.restoreFromBackup(backup);
1017
+
1018
+ if (success) {
1019
+ spinner.succeed('Rollback completed successfully');
1020
+ } else {
1021
+ spinner.fail('Rollback failed');
1022
+ }
1023
+ }
1024
+
1025
+ /**
1026
+ * Cleanup old backups
1027
+ */
1028
+ private async cleanup(options: any): Promise<void> {
1029
+ const safetyManager = createSafetyManager({ projectRoot: process.cwd() });
1030
+ const backups = await safetyManager.listBackups();
1031
+ const keepCount = parseInt(options.keep, 10);
1032
+
1033
+ if (backups.length <= keepCount) {
1034
+ console.log(
1035
+ chalk.green(
1036
+ `Only ${backups.length} backup(s) found. Nothing to clean up.`
1037
+ )
1038
+ );
1039
+ return;
1040
+ }
1041
+
1042
+ const toDelete = backups.slice(keepCount);
1043
+
1044
+ const { confirmed } = await inquirer.prompt([
1045
+ {
1046
+ type: 'confirm',
1047
+ name: 'confirmed',
1048
+ message: `Delete ${toDelete.length} old backup(s)?`,
1049
+ default: true,
1050
+ },
1051
+ ]);
1052
+
1053
+ if (!confirmed) {
1054
+ return;
1055
+ }
1056
+
1057
+ const spinner = ora('Cleaning up old backups...').start();
1058
+
1059
+ let deleted = 0;
1060
+ for (const backup of toDelete) {
1061
+ if (await safetyManager.deleteBackup(backup.id)) {
1062
+ deleted++;
1063
+ }
1064
+ }
1065
+
1066
+ spinner.succeed(`Deleted ${deleted} backup(s)`);
1067
+ }
1068
+ }
1069
+
1070
+ /**
1071
+ * Create and export the update command
1072
+ */
1073
+ export function createProjectUpdateCommand(): Command {
1074
+ const cmd = new Command('update')
1075
+ .alias('upgrade')
1076
+ .description('Update wundr project to a new version');
1077
+
1078
+ // Add subcommands directly
1079
+ cmd
1080
+ .command('project')
1081
+ .description('Update the entire project')
1082
+ .option(
1083
+ '--dry-run',
1084
+ 'Show what would be done without making changes',
1085
+ false
1086
+ )
1087
+ .option('-f, --force', 'Force update without prompts', false)
1088
+ .option('--skip-backup', 'Skip creating backup before update', false)
1089
+ .option(
1090
+ '-c, --components <names>',
1091
+ 'Specific components to update (comma-separated)'
1092
+ )
1093
+ .option('-v, --version <version>', 'Target version to update to')
1094
+ .option('--no-interactive', 'Disable interactive mode')
1095
+ .option('--verbose', 'Enable verbose output', false)
1096
+ .option('--auto-resolve', 'Automatically resolve conflicts', false)
1097
+ .action(async options => {
1098
+ const updateOptions: Partial<ProjectUpdateOptions> = {
1099
+ dryRun: options.dryRun,
1100
+ force: options.force,
1101
+ skipBackup: options.skipBackup,
1102
+ components: options.components
1103
+ ? options.components.split(',').map((c: string) => c.trim())
1104
+ : [],
1105
+ version: options.version || null,
1106
+ interactive: options.interactive !== false,
1107
+ verbose: options.verbose,
1108
+ autoResolve: options.autoResolve,
1109
+ };
1110
+
1111
+ const manager = new ProjectUpdateManager(process.cwd(), updateOptions);
1112
+ const result = await manager.run();
1113
+
1114
+ if (!result.success) {
1115
+ process.exit(1);
1116
+ }
1117
+ });
1118
+
1119
+ cmd
1120
+ .command('check')
1121
+ .description('Check if updates are available')
1122
+ .action(async () => {
1123
+ const spinner = ora('Checking for updates...').start();
1124
+ try {
1125
+ const state = await detectProjectState();
1126
+ spinner.succeed();
1127
+
1128
+ console.log(
1129
+ chalk.white('\nCurrent Version:'),
1130
+ chalk.yellow(state.wundrVersion || 'Not installed')
1131
+ );
1132
+ console.log(
1133
+ chalk.white('Health Score:'),
1134
+ chalk.yellow(`${state.healthScore}/100`)
1135
+ );
1136
+ console.log(
1137
+ chalk.white('Needs Update:'),
1138
+ state.isWundrOutdated ? chalk.red('Yes') : chalk.green('No')
1139
+ );
1140
+
1141
+ if (state.recommendations.length > 0) {
1142
+ console.log(chalk.white('\nRecommendations:'));
1143
+ for (const rec of state.recommendations.slice(0, 5)) {
1144
+ console.log(chalk.gray(` - ${rec}`));
1145
+ }
1146
+ }
1147
+ } catch (error) {
1148
+ spinner.fail('Check failed');
1149
+ throw error;
1150
+ }
1151
+ });
1152
+
1153
+ return cmd;
1154
+ }
1155
+
1156
+ export default ProjectUpdateCommands;