@wundr.io/cli 1.0.0 → 1.0.2-dev.20260530174250.ef0ec927
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +696 -280
- package/bin/wundr.js +13 -5
- package/package.json +30 -9
- package/src/ai/ai-service.ts +6 -4
- package/src/ai/claude-client.ts +6 -2
- package/src/ai/conversation-manager.ts +12 -5
- package/src/cli.ts +42 -13
- package/src/commands/ai.ts +340 -64
- package/src/commands/alignment.ts +1212 -0
- package/src/commands/analyze-optimized.ts +371 -33
- package/src/commands/analyze.ts +8 -6
- package/src/commands/batch.ts +166 -26
- package/src/commands/chat.ts +20 -10
- package/src/commands/claude-init.ts +31 -27
- package/src/commands/claude-setup.ts +761 -81
- package/src/commands/computer-setup.ts +524 -12
- package/src/commands/create-command.ts +3 -3
- package/src/commands/create.ts +9 -6
- package/src/commands/dashboard.ts +11 -6
- package/src/commands/govern.ts +11 -6
- package/src/commands/governance.ts +1005 -0
- package/src/commands/guardian.ts +887 -0
- package/src/commands/init.ts +104 -11
- package/src/commands/orchestrator.ts +789 -0
- package/src/commands/performance-optimizer.ts +15 -10
- package/src/commands/plugins.ts +8 -5
- package/src/commands/project-update.ts +1156 -0
- package/src/commands/rag.ts +1011 -0
- package/src/commands/session.ts +631 -0
- package/src/commands/setup.ts +42 -344
- package/src/commands/test-init.ts +3 -2
- package/src/commands/test.ts +3 -2
- package/src/commands/watch.ts +21 -11
- package/src/commands/worktree.ts +1057 -0
- package/src/context/context-manager.ts +5 -2
- package/src/context/session-manager.ts +18 -7
- package/src/framework/command-interface.ts +520 -0
- package/src/framework/command-registry.ts +942 -0
- package/src/framework/completion-exporter.ts +383 -0
- package/src/framework/debug-logger.ts +519 -0
- package/src/framework/error-handler.ts +867 -0
- package/src/framework/help-generator.ts +540 -0
- package/src/framework/index.ts +169 -0
- package/src/framework/interactive-repl.ts +703 -0
- package/src/framework/output-formatter.ts +834 -0
- package/src/framework/progress-manager.ts +539 -0
- package/src/index.ts +3 -2
- package/src/interactive/interactive-mode.ts +14 -7
- package/src/lib/conflict-resolution.ts +818 -0
- package/src/lib/merge-strategy.ts +550 -0
- package/src/lib/safety-mechanisms.ts +451 -0
- package/src/lib/state-detection.ts +1030 -0
- package/src/nlp/command-mapper.ts +8 -3
- package/src/nlp/command-parser.ts +5 -2
- package/src/nlp/intent-parser.ts +23 -9
- package/src/plugins/plugin-manager.ts +50 -24
- package/src/tests/computer-setup-integration.test.ts +470 -0
- package/src/types/index.ts +1 -1
- package/src/types/modules.d.ts +425 -1
- package/src/utils/backup-rollback-manager.ts +366 -0
- package/src/utils/claude-config-installer.ts +823 -0
- package/src/utils/config-manager.ts +9 -6
- package/src/utils/error-handler.ts +3 -1
- package/src/utils/logger.ts +35 -12
- package/templates/batch/ci-cd.yaml +7 -7
- package/test-suites/api/health.spec.ts +20 -23
- package/test-suites/helpers/test-config.ts +14 -13
- package/test-suites/ui/accessibility.spec.ts +27 -22
- package/test-suites/ui/smoke.spec.ts +26 -21
- package/dist/ai/ai-service.d.ts +0 -152
- package/dist/ai/ai-service.d.ts.map +0 -1
- package/dist/ai/ai-service.js +0 -430
- package/dist/ai/ai-service.js.map +0 -1
- package/dist/ai/claude-client.d.ts +0 -130
- package/dist/ai/claude-client.d.ts.map +0 -1
- package/dist/ai/claude-client.js +0 -339
- package/dist/ai/claude-client.js.map +0 -1
- package/dist/ai/conversation-manager.d.ts +0 -164
- package/dist/ai/conversation-manager.d.ts.map +0 -1
- package/dist/ai/conversation-manager.js +0 -612
- package/dist/ai/conversation-manager.js.map +0 -1
- package/dist/ai/index.d.ts +0 -5
- package/dist/ai/index.d.ts.map +0 -1
- package/dist/ai/index.js +0 -8
- package/dist/ai/index.js.map +0 -1
- package/dist/cli.d.ts +0 -36
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js +0 -173
- package/dist/cli.js.map +0 -1
- package/dist/commands/ai.d.ts +0 -89
- package/dist/commands/ai.d.ts.map +0 -1
- package/dist/commands/ai.js +0 -735
- package/dist/commands/ai.js.map +0 -1
- package/dist/commands/analyze-optimized.d.ts +0 -14
- package/dist/commands/analyze-optimized.d.ts.map +0 -1
- package/dist/commands/analyze-optimized.js +0 -437
- package/dist/commands/analyze-optimized.js.map +0 -1
- package/dist/commands/analyze.d.ts +0 -65
- package/dist/commands/analyze.d.ts.map +0 -1
- package/dist/commands/analyze.js +0 -435
- package/dist/commands/analyze.js.map +0 -1
- package/dist/commands/batch.d.ts +0 -71
- package/dist/commands/batch.d.ts.map +0 -1
- package/dist/commands/batch.js +0 -738
- package/dist/commands/batch.js.map +0 -1
- package/dist/commands/chat.d.ts +0 -71
- package/dist/commands/chat.d.ts.map +0 -1
- package/dist/commands/chat.js +0 -674
- package/dist/commands/chat.js.map +0 -1
- package/dist/commands/claude-init.d.ts +0 -28
- package/dist/commands/claude-init.d.ts.map +0 -1
- package/dist/commands/claude-init.js +0 -587
- package/dist/commands/claude-init.js.map +0 -1
- package/dist/commands/claude-setup.d.ts +0 -32
- package/dist/commands/claude-setup.d.ts.map +0 -1
- package/dist/commands/claude-setup.js +0 -570
- package/dist/commands/claude-setup.js.map +0 -1
- package/dist/commands/computer-setup-commands.d.ts +0 -39
- package/dist/commands/computer-setup-commands.d.ts.map +0 -1
- package/dist/commands/computer-setup-commands.js +0 -563
- package/dist/commands/computer-setup-commands.js.map +0 -1
- package/dist/commands/computer-setup.d.ts +0 -7
- package/dist/commands/computer-setup.d.ts.map +0 -1
- package/dist/commands/computer-setup.js +0 -481
- package/dist/commands/computer-setup.js.map +0 -1
- package/dist/commands/create-command.d.ts +0 -7
- package/dist/commands/create-command.d.ts.map +0 -1
- package/dist/commands/create-command.js +0 -158
- package/dist/commands/create-command.js.map +0 -1
- package/dist/commands/create.d.ts +0 -74
- package/dist/commands/create.d.ts.map +0 -1
- package/dist/commands/create.js +0 -556
- package/dist/commands/create.js.map +0 -1
- package/dist/commands/dashboard.d.ts +0 -91
- package/dist/commands/dashboard.d.ts.map +0 -1
- package/dist/commands/dashboard.js +0 -537
- package/dist/commands/dashboard.js.map +0 -1
- package/dist/commands/govern.d.ts +0 -70
- package/dist/commands/govern.d.ts.map +0 -1
- package/dist/commands/govern.js +0 -480
- package/dist/commands/govern.js.map +0 -1
- package/dist/commands/init.d.ts +0 -55
- package/dist/commands/init.d.ts.map +0 -1
- package/dist/commands/init.js +0 -584
- package/dist/commands/init.js.map +0 -1
- package/dist/commands/performance-optimizer.d.ts +0 -30
- package/dist/commands/performance-optimizer.d.ts.map +0 -1
- package/dist/commands/performance-optimizer.js +0 -649
- package/dist/commands/performance-optimizer.js.map +0 -1
- package/dist/commands/plugins.d.ts +0 -87
- package/dist/commands/plugins.d.ts.map +0 -1
- package/dist/commands/plugins.js +0 -685
- package/dist/commands/plugins.js.map +0 -1
- package/dist/commands/setup.d.ts +0 -29
- package/dist/commands/setup.d.ts.map +0 -1
- package/dist/commands/setup.js +0 -399
- package/dist/commands/setup.js.map +0 -1
- package/dist/commands/test-init.d.ts +0 -9
- package/dist/commands/test-init.d.ts.map +0 -1
- package/dist/commands/test-init.js +0 -222
- package/dist/commands/test-init.js.map +0 -1
- package/dist/commands/test.d.ts +0 -25
- package/dist/commands/test.d.ts.map +0 -1
- package/dist/commands/test.js +0 -217
- package/dist/commands/test.js.map +0 -1
- package/dist/commands/watch.d.ts +0 -76
- package/dist/commands/watch.d.ts.map +0 -1
- package/dist/commands/watch.js +0 -610
- package/dist/commands/watch.js.map +0 -1
- package/dist/context/context-manager.d.ts +0 -155
- package/dist/context/context-manager.d.ts.map +0 -1
- package/dist/context/context-manager.js +0 -383
- package/dist/context/context-manager.js.map +0 -1
- package/dist/context/index.d.ts +0 -3
- package/dist/context/index.d.ts.map +0 -1
- package/dist/context/index.js +0 -6
- package/dist/context/index.js.map +0 -1
- package/dist/context/session-manager.d.ts +0 -207
- package/dist/context/session-manager.d.ts.map +0 -1
- package/dist/context/session-manager.js +0 -682
- package/dist/context/session-manager.js.map +0 -1
- package/dist/index.d.ts +0 -8
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -51
- package/dist/index.js.map +0 -1
- package/dist/interactive/interactive-mode.d.ts +0 -76
- package/dist/interactive/interactive-mode.d.ts.map +0 -1
- package/dist/interactive/interactive-mode.js +0 -730
- package/dist/interactive/interactive-mode.js.map +0 -1
- package/dist/nlp/command-mapper.d.ts +0 -174
- package/dist/nlp/command-mapper.d.ts.map +0 -1
- package/dist/nlp/command-mapper.js +0 -623
- package/dist/nlp/command-mapper.js.map +0 -1
- package/dist/nlp/command-parser.d.ts +0 -106
- package/dist/nlp/command-parser.d.ts.map +0 -1
- package/dist/nlp/command-parser.js +0 -416
- package/dist/nlp/command-parser.js.map +0 -1
- package/dist/nlp/index.d.ts +0 -5
- package/dist/nlp/index.d.ts.map +0 -1
- package/dist/nlp/index.js +0 -8
- package/dist/nlp/index.js.map +0 -1
- package/dist/nlp/intent-classifier.d.ts +0 -59
- package/dist/nlp/intent-classifier.d.ts.map +0 -1
- package/dist/nlp/intent-classifier.js +0 -384
- package/dist/nlp/intent-classifier.js.map +0 -1
- package/dist/nlp/intent-parser.d.ts +0 -152
- package/dist/nlp/intent-parser.d.ts.map +0 -1
- package/dist/nlp/intent-parser.js +0 -739
- package/dist/nlp/intent-parser.js.map +0 -1
- package/dist/plugins/plugin-manager.d.ts +0 -120
- package/dist/plugins/plugin-manager.d.ts.map +0 -1
- package/dist/plugins/plugin-manager.js +0 -595
- package/dist/plugins/plugin-manager.js.map +0 -1
- package/dist/types/index.d.ts +0 -224
- package/dist/types/index.d.ts.map +0 -1
- package/dist/types/index.js +0 -3
- package/dist/types/index.js.map +0 -1
- package/dist/utils/config-manager.d.ts +0 -73
- package/dist/utils/config-manager.d.ts.map +0 -1
- package/dist/utils/config-manager.js +0 -339
- package/dist/utils/config-manager.js.map +0 -1
- package/dist/utils/error-handler.d.ts +0 -46
- package/dist/utils/error-handler.d.ts.map +0 -1
- package/dist/utils/error-handler.js +0 -169
- package/dist/utils/error-handler.js.map +0 -1
- package/dist/utils/logger.d.ts +0 -25
- package/dist/utils/logger.d.ts.map +0 -1
- package/dist/utils/logger.js +0 -94
- package/dist/utils/logger.js.map +0 -1
- package/src/commands/computer-setup-commands.ts +0 -709
|
@@ -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;
|