@wundr.io/cli 1.0.10 → 1.0.12
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/bin/wundr.js +8 -4
- package/package.json +23 -23
- package/src/ai/ai-service.ts +16 -17
- package/src/ai/claude-client.ts +16 -16
- package/src/ai/conversation-manager.ts +29 -29
- package/src/cli.ts +4 -4
- package/src/commands/ai.ts +246 -78
- package/src/commands/alignment.ts +74 -74
- package/src/commands/analyze-optimized.ts +111 -78
- package/src/commands/analyze.ts +14 -14
- package/src/commands/batch.ts +179 -42
- package/src/commands/chat.ts +37 -30
- package/src/commands/claude-init.ts +41 -45
- package/src/commands/claude-setup.ts +204 -119
- package/src/commands/computer-setup.ts +85 -43
- package/src/commands/create-command.ts +4 -4
- package/src/commands/create.ts +27 -27
- package/src/commands/dashboard.ts +24 -24
- package/src/commands/govern.ts +25 -25
- package/src/commands/governance.ts +34 -34
- package/src/commands/guardian.ts +56 -56
- package/src/commands/init.ts +25 -22
- package/src/commands/orchestrator.ts +68 -41
- package/src/commands/performance-optimizer.ts +34 -35
- package/src/commands/plugins.ts +27 -27
- package/src/commands/project-update.ts +175 -72
- package/src/commands/rag.ts +185 -78
- package/src/commands/session.ts +35 -35
- package/src/commands/setup.ts +40 -344
- package/src/commands/test-init.ts +3 -3
- package/src/commands/test.ts +4 -4
- package/src/commands/watch.ts +28 -29
- package/src/commands/worktree.ts +49 -49
- package/src/context/context-manager.ts +10 -10
- package/src/context/session-manager.ts +41 -41
- 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 +4 -4
- package/src/interactive/interactive-mode.ts +16 -16
- package/src/lib/conflict-resolution.ts +799 -9
- package/src/lib/merge-strategy.ts +529 -7
- package/src/lib/safety-mechanisms.ts +422 -18
- package/src/lib/state-detection.ts +1015 -13
- package/src/nlp/command-mapper.ts +29 -29
- package/src/nlp/command-parser.ts +17 -17
- package/src/nlp/intent-classifier.ts +7 -7
- package/src/nlp/intent-parser.ts +54 -52
- package/src/plugins/plugin-manager.ts +61 -39
- package/src/tests/computer-setup-integration.test.ts +46 -15
- package/src/types/modules.d.ts +424 -1
- package/src/utils/backup-rollback-manager.ts +11 -8
- package/src/utils/config-manager.ts +3 -3
- package/src/utils/error-handler.ts +2 -2
- package/src/utils/logger.ts +22 -22
- 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/LICENSE +0 -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 -340
- 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 -614
- 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 -192
- 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 -799
- package/dist/commands/ai.js.map +0 -1
- package/dist/commands/alignment.d.ts +0 -78
- package/dist/commands/alignment.d.ts.map +0 -1
- package/dist/commands/alignment.js +0 -817
- package/dist/commands/alignment.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 -600
- 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 -591
- package/dist/commands/claude-init.js.map +0 -1
- package/dist/commands/claude-setup.d.ts +0 -119
- package/dist/commands/claude-setup.d.ts.map +0 -1
- package/dist/commands/claude-setup.js +0 -1073
- package/dist/commands/claude-setup.js.map +0 -1
- package/dist/commands/computer-setup-commands.d.ts +0 -53
- package/dist/commands/computer-setup-commands.d.ts.map +0 -1
- package/dist/commands/computer-setup-commands.js +0 -705
- 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 -849
- 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 -538
- 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 -481
- package/dist/commands/govern.js.map +0 -1
- package/dist/commands/governance.d.ts +0 -17
- package/dist/commands/governance.d.ts.map +0 -1
- package/dist/commands/governance.js +0 -703
- package/dist/commands/governance.js.map +0 -1
- package/dist/commands/guardian.d.ts +0 -20
- package/dist/commands/guardian.d.ts.map +0 -1
- package/dist/commands/guardian.js +0 -597
- package/dist/commands/guardian.js.map +0 -1
- package/dist/commands/init.d.ts +0 -59
- package/dist/commands/init.d.ts.map +0 -1
- package/dist/commands/init.js +0 -650
- package/dist/commands/init.js.map +0 -1
- package/dist/commands/orchestrator.d.ts +0 -7
- package/dist/commands/orchestrator.d.ts.map +0 -1
- package/dist/commands/orchestrator.js +0 -571
- package/dist/commands/orchestrator.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 -650
- 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/rag.d.ts +0 -7
- package/dist/commands/rag.d.ts.map +0 -1
- package/dist/commands/rag.js +0 -748
- package/dist/commands/rag.js.map +0 -1
- package/dist/commands/session.d.ts +0 -41
- package/dist/commands/session.d.ts.map +0 -1
- package/dist/commands/session.js +0 -441
- package/dist/commands/session.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 -397
- 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/vp.d.ts +0 -7
- package/dist/commands/vp.d.ts.map +0 -1
- package/dist/commands/vp.js +0 -571
- package/dist/commands/vp.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 -613
- package/dist/commands/watch.js.map +0 -1
- package/dist/commands/worktree.d.ts +0 -63
- package/dist/commands/worktree.d.ts.map +0 -1
- package/dist/commands/worktree.js +0 -774
- package/dist/commands/worktree.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 -686
- 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 -732
- 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 -624
- 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 -417
- 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 -744
- 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/backup-rollback-manager.d.ts +0 -72
- package/dist/utils/backup-rollback-manager.d.ts.map +0 -1
- package/dist/utils/backup-rollback-manager.js +0 -289
- package/dist/utils/backup-rollback-manager.js.map +0 -1
- package/dist/utils/claude-config-installer.d.ts +0 -98
- package/dist/utils/claude-config-installer.d.ts.map +0 -1
- package/dist/utils/claude-config-installer.js +0 -678
- package/dist/utils/claude-config-installer.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 -105
- package/dist/utils/logger.js.map +0 -1
- package/src/commands/computer-setup-commands.ts +0 -872
|
@@ -1,47 +1,451 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Safety Mechanisms -
|
|
3
|
-
*
|
|
2
|
+
* Safety Mechanisms - File-system based backup, transaction, and rollback support
|
|
3
|
+
*
|
|
4
|
+
* Backups are stored under `{projectRoot}/.wundr-backup/{backupId}/` and a
|
|
5
|
+
* metadata index is maintained at `{projectRoot}/.wundr-backup/index.json`.
|
|
6
|
+
*
|
|
7
|
+
* Transaction pattern:
|
|
8
|
+
* const tx = safetyManager.startTransaction('label');
|
|
9
|
+
* tx.recordOperation({ type, path, backupRef });
|
|
10
|
+
* tx.completeOperation(path);
|
|
11
|
+
* await tx.commit(); // no-op for now; marks the transaction done
|
|
12
|
+
* // or
|
|
13
|
+
* await tx.rollback(); // restores each file from its backupRef
|
|
4
14
|
*/
|
|
5
15
|
|
|
16
|
+
import { existsSync } from 'fs';
|
|
17
|
+
import * as fs from 'fs/promises';
|
|
18
|
+
import * as path from 'path';
|
|
19
|
+
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
// Public interfaces (kept from original stub + extended for consumer usage)
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
|
|
24
|
+
export interface SafetyManagerOptions {
|
|
25
|
+
/** Root directory of the project being managed */
|
|
26
|
+
projectRoot?: string;
|
|
27
|
+
/** When true, skip writing any backup files to disk */
|
|
28
|
+
skipBackup?: boolean;
|
|
29
|
+
/** When true, do not write anything to disk */
|
|
30
|
+
dryRun?: boolean;
|
|
31
|
+
}
|
|
32
|
+
|
|
6
33
|
export interface SafetyManager {
|
|
7
|
-
|
|
8
|
-
|
|
34
|
+
/** Create a full backup of the supplied file list */
|
|
35
|
+
createBackup(
|
|
36
|
+
files: string[],
|
|
37
|
+
description?: string,
|
|
38
|
+
fromVersion?: string,
|
|
39
|
+
toVersion?: string
|
|
40
|
+
): Promise<UpdateBackup>;
|
|
41
|
+
/** Begin a logical transaction that can be committed or rolled back */
|
|
42
|
+
startTransaction(label?: string): UpdateTransaction;
|
|
43
|
+
/** List all stored backups, newest first */
|
|
9
44
|
listBackups(): Promise<UpdateBackup[]>;
|
|
45
|
+
/** Return the most recent backup, or null if none exist */
|
|
10
46
|
getLatestBackup(): Promise<UpdateBackup | null>;
|
|
11
|
-
|
|
12
|
-
|
|
47
|
+
/**
|
|
48
|
+
* Restore files from a backup.
|
|
49
|
+
* Accepts either a full UpdateBackup object or a bare backup-id string
|
|
50
|
+
* so that both usages in project-update.ts are satisfied.
|
|
51
|
+
*/
|
|
52
|
+
restoreFromBackup(backupOrId: UpdateBackup | string): Promise<boolean>;
|
|
53
|
+
/** Permanently remove a backup by id */
|
|
54
|
+
deleteBackup(backupId: string): Promise<boolean>;
|
|
55
|
+
// --- Legacy interface aliases kept for backwards-compatibility ---
|
|
56
|
+
/** @deprecated Use startTransaction instead */
|
|
57
|
+
beginTransaction(): UpdateTransaction;
|
|
13
58
|
}
|
|
14
59
|
|
|
15
60
|
export interface UpdateBackup {
|
|
16
61
|
id: string;
|
|
17
62
|
timestamp: Date;
|
|
63
|
+
/** Absolute path to the backup directory on disk */
|
|
64
|
+
path: string;
|
|
65
|
+
/** List of original file paths that were backed up */
|
|
66
|
+
files: string[];
|
|
67
|
+
description: string;
|
|
68
|
+
fromVersion: string;
|
|
69
|
+
toVersion: string;
|
|
70
|
+
/** Convenience method that delegates to SafetyManager.restoreFromBackup */
|
|
18
71
|
restore(): Promise<void>;
|
|
19
72
|
}
|
|
20
73
|
|
|
74
|
+
export interface TransactionOperation {
|
|
75
|
+
type: 'update' | 'create' | 'delete';
|
|
76
|
+
path: string;
|
|
77
|
+
/** Reference to the backup entry that holds the pre-change copy */
|
|
78
|
+
backupRef: string | null;
|
|
79
|
+
}
|
|
80
|
+
|
|
21
81
|
export interface UpdateTransaction {
|
|
22
|
-
|
|
82
|
+
/** Record a pending operation; call before modifying the file */
|
|
83
|
+
recordOperation(op: TransactionOperation): void;
|
|
84
|
+
/** Mark a previously-recorded operation as successfully completed */
|
|
85
|
+
completeOperation(filePath: string): void;
|
|
86
|
+
/** Mark a previously-recorded operation as failed */
|
|
87
|
+
failOperation(filePath: string, reason: string): void;
|
|
88
|
+
/** Commit the transaction (clears the in-memory log) */
|
|
89
|
+
commit(): Promise<boolean>;
|
|
90
|
+
/** Roll back by restoring each file from its saved backup copy */
|
|
23
91
|
rollback(): Promise<void>;
|
|
24
92
|
}
|
|
25
93
|
|
|
26
|
-
|
|
94
|
+
// ---------------------------------------------------------------------------
|
|
95
|
+
// Serialised form stored in index.json
|
|
96
|
+
// ---------------------------------------------------------------------------
|
|
97
|
+
|
|
98
|
+
interface BackupIndexEntry {
|
|
99
|
+
id: string;
|
|
100
|
+
timestamp: string; // ISO string
|
|
101
|
+
path: string;
|
|
102
|
+
files: string[];
|
|
103
|
+
description: string;
|
|
104
|
+
fromVersion: string;
|
|
105
|
+
toVersion: string;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ---------------------------------------------------------------------------
|
|
109
|
+
// Internal helpers
|
|
110
|
+
// ---------------------------------------------------------------------------
|
|
111
|
+
|
|
112
|
+
function generateBackupId(): string {
|
|
113
|
+
// e.g. "bkp-20240311T143022-456"
|
|
114
|
+
const ts = new Date().toISOString().replace(/[:.]/g, '').slice(0, 15);
|
|
115
|
+
const rand = Math.floor(Math.random() * 1000)
|
|
116
|
+
.toString()
|
|
117
|
+
.padStart(3, '0');
|
|
118
|
+
return `bkp-${ts}-${rand}`;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function backupDir(projectRoot: string): string {
|
|
122
|
+
return path.join(projectRoot, '.wundr-backup');
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function indexPath(projectRoot: string): string {
|
|
126
|
+
return path.join(backupDir(projectRoot), 'index.json');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async function ensureBackupDir(projectRoot: string): Promise<void> {
|
|
130
|
+
await fs.mkdir(backupDir(projectRoot), { recursive: true });
|
|
131
|
+
|
|
132
|
+
const idx = indexPath(projectRoot);
|
|
133
|
+
if (!existsSync(idx)) {
|
|
134
|
+
await fs.writeFile(idx, JSON.stringify([], null, 2), 'utf-8');
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function readIndex(projectRoot: string): Promise<BackupIndexEntry[]> {
|
|
139
|
+
try {
|
|
140
|
+
const raw = await fs.readFile(indexPath(projectRoot), 'utf-8');
|
|
141
|
+
return JSON.parse(raw) as BackupIndexEntry[];
|
|
142
|
+
} catch {
|
|
143
|
+
return [];
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async function writeIndex(
|
|
148
|
+
projectRoot: string,
|
|
149
|
+
entries: BackupIndexEntry[]
|
|
150
|
+
): Promise<void> {
|
|
151
|
+
await fs.writeFile(
|
|
152
|
+
indexPath(projectRoot),
|
|
153
|
+
JSON.stringify(entries, null, 2),
|
|
154
|
+
'utf-8'
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Convert an index entry + projectRoot into a full UpdateBackup object.
|
|
160
|
+
*/
|
|
161
|
+
function entryToBackup(
|
|
162
|
+
entry: BackupIndexEntry,
|
|
163
|
+
manager: SafetyManager
|
|
164
|
+
): UpdateBackup {
|
|
27
165
|
return {
|
|
28
|
-
|
|
29
|
-
|
|
166
|
+
id: entry.id,
|
|
167
|
+
timestamp: new Date(entry.timestamp),
|
|
168
|
+
path: entry.path,
|
|
169
|
+
files: entry.files,
|
|
170
|
+
description: entry.description,
|
|
171
|
+
fromVersion: entry.fromVersion,
|
|
172
|
+
toVersion: entry.toVersion,
|
|
173
|
+
restore: async () => {
|
|
174
|
+
await manager.restoreFromBackup(entry.id);
|
|
30
175
|
},
|
|
31
|
-
|
|
32
|
-
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// ---------------------------------------------------------------------------
|
|
180
|
+
// Factory
|
|
181
|
+
// ---------------------------------------------------------------------------
|
|
182
|
+
|
|
183
|
+
export function createSafetyManager(
|
|
184
|
+
options: SafetyManagerOptions = {}
|
|
185
|
+
): SafetyManager {
|
|
186
|
+
const projectRoot = options.projectRoot || process.cwd();
|
|
187
|
+
const skipBackup = options.skipBackup ?? false;
|
|
188
|
+
const dryRun = options.dryRun ?? false;
|
|
189
|
+
|
|
190
|
+
// Forward declaration so entryToBackup can reference `manager`.
|
|
191
|
+
const manager: SafetyManager = {
|
|
192
|
+
// ------------------------------------------------------------------
|
|
193
|
+
// createBackup
|
|
194
|
+
// ------------------------------------------------------------------
|
|
195
|
+
async createBackup(
|
|
196
|
+
files: string[],
|
|
197
|
+
description = 'Backup',
|
|
198
|
+
fromVersion = '',
|
|
199
|
+
toVersion = ''
|
|
200
|
+
): Promise<UpdateBackup> {
|
|
201
|
+
if (skipBackup || dryRun) {
|
|
202
|
+
// Return a no-op backup object without touching disk.
|
|
203
|
+
const noop: UpdateBackup = {
|
|
204
|
+
id: `noop-${Date.now()}`,
|
|
205
|
+
timestamp: new Date(),
|
|
206
|
+
path: '',
|
|
207
|
+
files: [],
|
|
208
|
+
description,
|
|
209
|
+
fromVersion,
|
|
210
|
+
toVersion,
|
|
211
|
+
restore: async () => {},
|
|
212
|
+
};
|
|
213
|
+
return noop;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
await ensureBackupDir(projectRoot);
|
|
217
|
+
|
|
218
|
+
const id = generateBackupId();
|
|
219
|
+
const backupPath = path.join(backupDir(projectRoot), id);
|
|
220
|
+
await fs.mkdir(backupPath, { recursive: true });
|
|
221
|
+
|
|
222
|
+
const backedUpFiles: string[] = [];
|
|
223
|
+
|
|
224
|
+
for (const filePath of files) {
|
|
225
|
+
if (!existsSync(filePath)) {
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Preserve the relative structure inside the backup directory.
|
|
230
|
+
// For absolute paths outside projectRoot use the full path stripped
|
|
231
|
+
// of its leading separator.
|
|
232
|
+
let relative: string;
|
|
233
|
+
try {
|
|
234
|
+
relative = path.relative(projectRoot, filePath);
|
|
235
|
+
// If the file is outside projectRoot, relative starts with '..'
|
|
236
|
+
if (relative.startsWith('..')) {
|
|
237
|
+
relative = filePath.replace(/^[/\\]/, '');
|
|
238
|
+
}
|
|
239
|
+
} catch {
|
|
240
|
+
relative = filePath.replace(/^[/\\]/, '');
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const dest = path.join(backupPath, relative);
|
|
244
|
+
await fs.mkdir(path.dirname(dest), { recursive: true });
|
|
245
|
+
await fs.copyFile(filePath, dest);
|
|
246
|
+
backedUpFiles.push(filePath);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const entry: BackupIndexEntry = {
|
|
250
|
+
id,
|
|
251
|
+
timestamp: new Date().toISOString(),
|
|
252
|
+
path: backupPath,
|
|
253
|
+
files: backedUpFiles,
|
|
254
|
+
description,
|
|
255
|
+
fromVersion,
|
|
256
|
+
toVersion,
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
const index = await readIndex(projectRoot);
|
|
260
|
+
// Newest first.
|
|
261
|
+
index.unshift(entry);
|
|
262
|
+
await writeIndex(projectRoot, index);
|
|
263
|
+
|
|
264
|
+
return entryToBackup(entry, manager);
|
|
33
265
|
},
|
|
266
|
+
|
|
267
|
+
// ------------------------------------------------------------------
|
|
268
|
+
// startTransaction
|
|
269
|
+
// ------------------------------------------------------------------
|
|
270
|
+
startTransaction(label = 'transaction'): UpdateTransaction {
|
|
271
|
+
interface OpRecord {
|
|
272
|
+
op: TransactionOperation;
|
|
273
|
+
status: 'pending' | 'completed' | 'failed';
|
|
274
|
+
failReason?: string;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const ops = new Map<string, OpRecord>();
|
|
278
|
+
let committed = false;
|
|
279
|
+
|
|
280
|
+
const transaction: UpdateTransaction = {
|
|
281
|
+
recordOperation(op: TransactionOperation): void {
|
|
282
|
+
if (committed) {
|
|
283
|
+
throw new Error(
|
|
284
|
+
`Transaction "${label}" is already committed; cannot record new operations.`
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
ops.set(op.path, { op, status: 'pending' });
|
|
288
|
+
},
|
|
289
|
+
|
|
290
|
+
completeOperation(filePath: string): void {
|
|
291
|
+
const record = ops.get(filePath);
|
|
292
|
+
if (record) {
|
|
293
|
+
record.status = 'completed';
|
|
294
|
+
}
|
|
295
|
+
},
|
|
296
|
+
|
|
297
|
+
failOperation(filePath: string, reason: string): void {
|
|
298
|
+
const record = ops.get(filePath);
|
|
299
|
+
if (record) {
|
|
300
|
+
record.status = 'failed';
|
|
301
|
+
record.failReason = reason;
|
|
302
|
+
}
|
|
303
|
+
},
|
|
304
|
+
|
|
305
|
+
async commit(): Promise<boolean> {
|
|
306
|
+
committed = true;
|
|
307
|
+
// All operations are already written to disk by the caller; the
|
|
308
|
+
// transaction commit just seals the log.
|
|
309
|
+
return true;
|
|
310
|
+
},
|
|
311
|
+
|
|
312
|
+
async rollback(): Promise<void> {
|
|
313
|
+
// For each operation that has a backupRef, restore the file from
|
|
314
|
+
// the backup entry stored under projectRoot/.wundr-backup/{id}.
|
|
315
|
+
for (const [filePath, record] of ops.entries()) {
|
|
316
|
+
const { backupRef } = record.op;
|
|
317
|
+
if (!backupRef) {
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
try {
|
|
322
|
+
const index = await readIndex(projectRoot);
|
|
323
|
+
const entry = index.find(e => e.id === backupRef);
|
|
324
|
+
if (!entry) {
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
let relative: string;
|
|
329
|
+
try {
|
|
330
|
+
relative = path.relative(projectRoot, filePath);
|
|
331
|
+
if (relative.startsWith('..')) {
|
|
332
|
+
relative = filePath.replace(/^[/\\]/, '');
|
|
333
|
+
}
|
|
334
|
+
} catch {
|
|
335
|
+
relative = filePath.replace(/^[/\\]/, '');
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const backupFilePath = path.join(entry.path, relative);
|
|
339
|
+
if (existsSync(backupFilePath)) {
|
|
340
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
341
|
+
await fs.copyFile(backupFilePath, filePath);
|
|
342
|
+
}
|
|
343
|
+
} catch {
|
|
344
|
+
// Best-effort; continue with remaining files.
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
},
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
return transaction;
|
|
351
|
+
},
|
|
352
|
+
|
|
353
|
+
// ------------------------------------------------------------------
|
|
354
|
+
// listBackups
|
|
355
|
+
// ------------------------------------------------------------------
|
|
34
356
|
async listBackups(): Promise<UpdateBackup[]> {
|
|
35
|
-
|
|
357
|
+
await ensureBackupDir(projectRoot);
|
|
358
|
+
const index = await readIndex(projectRoot);
|
|
359
|
+
// Already sorted newest-first by how we write the index.
|
|
360
|
+
return index.map(entry => entryToBackup(entry, manager));
|
|
36
361
|
},
|
|
362
|
+
|
|
363
|
+
// ------------------------------------------------------------------
|
|
364
|
+
// getLatestBackup
|
|
365
|
+
// ------------------------------------------------------------------
|
|
37
366
|
async getLatestBackup(): Promise<UpdateBackup | null> {
|
|
38
|
-
|
|
367
|
+
await ensureBackupDir(projectRoot);
|
|
368
|
+
const index = await readIndex(projectRoot);
|
|
369
|
+
if (index.length === 0) {
|
|
370
|
+
return null;
|
|
371
|
+
}
|
|
372
|
+
return entryToBackup(index[0], manager);
|
|
39
373
|
},
|
|
40
|
-
|
|
41
|
-
|
|
374
|
+
|
|
375
|
+
// ------------------------------------------------------------------
|
|
376
|
+
// restoreFromBackup
|
|
377
|
+
// ------------------------------------------------------------------
|
|
378
|
+
async restoreFromBackup(
|
|
379
|
+
backupOrId: UpdateBackup | string
|
|
380
|
+
): Promise<boolean> {
|
|
381
|
+
const id = typeof backupOrId === 'string' ? backupOrId : backupOrId.id;
|
|
382
|
+
|
|
383
|
+
const index = await readIndex(projectRoot);
|
|
384
|
+
const entry = index.find(e => e.id === id);
|
|
385
|
+
if (!entry) {
|
|
386
|
+
return false;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
let allRestored = true;
|
|
390
|
+
|
|
391
|
+
for (const originalPath of entry.files) {
|
|
392
|
+
let relative: string;
|
|
393
|
+
try {
|
|
394
|
+
relative = path.relative(projectRoot, originalPath);
|
|
395
|
+
if (relative.startsWith('..')) {
|
|
396
|
+
relative = originalPath.replace(/^[/\\]/, '');
|
|
397
|
+
}
|
|
398
|
+
} catch {
|
|
399
|
+
relative = originalPath.replace(/^[/\\]/, '');
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const backupFilePath = path.join(entry.path, relative);
|
|
403
|
+
|
|
404
|
+
if (!existsSync(backupFilePath)) {
|
|
405
|
+
allRestored = false;
|
|
406
|
+
continue;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
try {
|
|
410
|
+
await fs.mkdir(path.dirname(originalPath), { recursive: true });
|
|
411
|
+
await fs.copyFile(backupFilePath, originalPath);
|
|
412
|
+
} catch {
|
|
413
|
+
allRestored = false;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
return allRestored;
|
|
418
|
+
},
|
|
419
|
+
|
|
420
|
+
// ------------------------------------------------------------------
|
|
421
|
+
// deleteBackup
|
|
422
|
+
// ------------------------------------------------------------------
|
|
423
|
+
async deleteBackup(backupId: string): Promise<boolean> {
|
|
424
|
+
const index = await readIndex(projectRoot);
|
|
425
|
+
const entry = index.find(e => e.id === backupId);
|
|
426
|
+
if (!entry) {
|
|
427
|
+
return false;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
try {
|
|
431
|
+
if (existsSync(entry.path)) {
|
|
432
|
+
await fs.rm(entry.path, { recursive: true, force: true });
|
|
433
|
+
}
|
|
434
|
+
const updated = index.filter(e => e.id !== backupId);
|
|
435
|
+
await writeIndex(projectRoot, updated);
|
|
436
|
+
return true;
|
|
437
|
+
} catch {
|
|
438
|
+
return false;
|
|
439
|
+
}
|
|
42
440
|
},
|
|
43
|
-
|
|
44
|
-
|
|
441
|
+
|
|
442
|
+
// ------------------------------------------------------------------
|
|
443
|
+
// Legacy: beginTransaction (alias)
|
|
444
|
+
// ------------------------------------------------------------------
|
|
445
|
+
beginTransaction(): UpdateTransaction {
|
|
446
|
+
return manager.startTransaction('transaction');
|
|
45
447
|
},
|
|
46
448
|
};
|
|
449
|
+
|
|
450
|
+
return manager;
|
|
47
451
|
}
|