@uagents/syncenv-cli 0.1.1
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 +204 -0
- package/dist/chunk-F7ZZUTRW.js +403 -0
- package/dist/chunk-JBMZAAVP.js +176 -0
- package/dist/chunk-NV6H5OGL.js +218 -0
- package/dist/chunk-OVEYHV4C.js +333 -0
- package/dist/cookie-store-Z6DNTUGS.js +16 -0
- package/dist/crypto-X7MZU7DV.js +58 -0
- package/dist/index.js +2091 -0
- package/dist/interactive-GOIXZ6UH.js +6 -0
- package/dist/secure-storage-UEK3LD5L.js +35 -0
- package/package.json +53 -0
package/README.md
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
# SyncEnv CLI
|
|
2
|
+
|
|
3
|
+
CLI for SyncEnv - Secure environment variable synchronization.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
### Via npm (Recommended)
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install -g @uagents/syncenv-cli
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
### Via pnpm
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pnpm add -g @uagents/syncenv-cli
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
### Via curl (Standalone Binary)
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
curl -sL https://syncenv-files.uagents.app/cli/install.sh | bash
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
With custom install directory:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
curl -sL https://syncenv-files.uagents.app/cli/install.sh | INSTALL_DIR=~/.local/bin bash
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Quick Start
|
|
32
|
+
|
|
33
|
+
1. **Initialize a project:**
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
syncenv init
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
2. **Create an account or login:**
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
syncenv auth signup
|
|
43
|
+
# or
|
|
44
|
+
syncenv auth login
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
3. **Create a project on the server:**
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
syncenv project create
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
4. **Push your .env file:**
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
syncenv env push
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
5. **Pull environment variables:**
|
|
60
|
+
```bash
|
|
61
|
+
syncenv env pull
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Commands
|
|
65
|
+
|
|
66
|
+
### Authentication
|
|
67
|
+
|
|
68
|
+
- `syncenv auth signup` - Create a new account
|
|
69
|
+
- `syncenv auth login` - Login to your account
|
|
70
|
+
- `syncenv auth logout` - Logout and clear session
|
|
71
|
+
- `syncenv auth status` - Check authentication status
|
|
72
|
+
|
|
73
|
+
### Project Management
|
|
74
|
+
|
|
75
|
+
- `syncenv init` - Initialize project configuration
|
|
76
|
+
- `syncenv project list` - List all projects
|
|
77
|
+
- `syncenv project create [name]` - Create a new project
|
|
78
|
+
- `syncenv project get <id>` - Get project details
|
|
79
|
+
- `syncenv project delete <id>` - Delete a project
|
|
80
|
+
- `syncenv project use <id>` - Set default project
|
|
81
|
+
|
|
82
|
+
### Environment Variables
|
|
83
|
+
|
|
84
|
+
- `syncenv env push` - Push .env file to server
|
|
85
|
+
- `-p, --project <id>` - Project ID
|
|
86
|
+
- `-e, --env <name>` - Environment name
|
|
87
|
+
- `-f, --file <path>` - File path
|
|
88
|
+
- `-m, --message <message>` - Change description
|
|
89
|
+
|
|
90
|
+
- `syncenv env pull` - Pull .env file from server
|
|
91
|
+
- `-p, --project <id>` - Project ID
|
|
92
|
+
- `-e, --env <name>` - Environment name
|
|
93
|
+
- `-f, --file <path>` - Output file path
|
|
94
|
+
- `-v, --version <number>` - Specific version to pull
|
|
95
|
+
- `-m, --merge` - Merge with existing file
|
|
96
|
+
|
|
97
|
+
- `syncenv env history` - Show version history
|
|
98
|
+
- `-l, --limit <number>` - Number of versions to show
|
|
99
|
+
|
|
100
|
+
- `syncenv env diff <v1> <v2>` - Compare two versions
|
|
101
|
+
|
|
102
|
+
- `syncenv env rollback <version>` - Rollback to a specific version
|
|
103
|
+
|
|
104
|
+
- `syncenv env sync` - Smart sync (pull + merge + push)
|
|
105
|
+
- `--dry-run` - Preview changes without applying
|
|
106
|
+
- `--strategy <strategy>` - Conflict resolution: `local-wins`, `remote-wins`, `interactive`
|
|
107
|
+
- `-y, --yes` - Skip confirmation prompts
|
|
108
|
+
|
|
109
|
+
### Device Management
|
|
110
|
+
|
|
111
|
+
- `syncenv device list` - List all devices
|
|
112
|
+
- `syncenv device authorize <id>` - Authorize a pending device
|
|
113
|
+
- `syncenv device revoke <id>` - Revoke a device
|
|
114
|
+
- `syncenv device remove <id>` - Remove a device
|
|
115
|
+
- `syncenv device current` - Show current device info
|
|
116
|
+
|
|
117
|
+
### Diagnostics
|
|
118
|
+
|
|
119
|
+
- `syncenv doctor` - Diagnose configuration and connectivity issues
|
|
120
|
+
|
|
121
|
+
## Smart Merge
|
|
122
|
+
|
|
123
|
+
When pushing or syncing, if remote has been modified since your last pull, the CLI automatically performs a **three-way merge**:
|
|
124
|
+
|
|
125
|
+
1. **Detect conflicts** - Compare your local changes with remote changes
|
|
126
|
+
2. **Auto-merge** - Non-conflicting changes are merged automatically
|
|
127
|
+
3. **Interactive resolution** - Conflicts are presented with options:
|
|
128
|
+
- `[l]` Use local value
|
|
129
|
+
- `[r]` Use remote value
|
|
130
|
+
- `[b]` Keep both values
|
|
131
|
+
- `[e]` Edit custom value
|
|
132
|
+
- `[s]` Skip and keep conflict markers
|
|
133
|
+
|
|
134
|
+
### Merge Strategies
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
# Interactive (default) - prompt for each conflict
|
|
138
|
+
syncenv env sync
|
|
139
|
+
|
|
140
|
+
# Local wins - always use your changes
|
|
141
|
+
syncenv env sync --strategy=local-wins
|
|
142
|
+
|
|
143
|
+
# Remote wins - always use remote changes
|
|
144
|
+
syncenv env sync --strategy=remote-wins
|
|
145
|
+
|
|
146
|
+
# Fail on conflict - exit if conflicts exist
|
|
147
|
+
syncenv env push --strategy=fail-on-conflict
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## Configuration
|
|
151
|
+
|
|
152
|
+
### CLI Config
|
|
153
|
+
|
|
154
|
+
Stored in OS-specific config directory (e.g., `~/.config/syncenv/` on Linux).
|
|
155
|
+
|
|
156
|
+
- `apiUrl` - API base URL
|
|
157
|
+
- `authToken` - Authentication token
|
|
158
|
+
- `userId` - User ID
|
|
159
|
+
- `deviceId` - Device ID
|
|
160
|
+
|
|
161
|
+
### Project Config (`.envsyncrc`)
|
|
162
|
+
|
|
163
|
+
Example:
|
|
164
|
+
|
|
165
|
+
```yaml
|
|
166
|
+
project:
|
|
167
|
+
name: myapp
|
|
168
|
+
id: proj_xxx
|
|
169
|
+
|
|
170
|
+
defaults:
|
|
171
|
+
environment: dev
|
|
172
|
+
pushOnChange: false
|
|
173
|
+
confirmOverwrite: true
|
|
174
|
+
|
|
175
|
+
encryption:
|
|
176
|
+
algorithm: AES-256-GCM
|
|
177
|
+
keyDerivation: Argon2id
|
|
178
|
+
|
|
179
|
+
environments:
|
|
180
|
+
dev:
|
|
181
|
+
file: .env
|
|
182
|
+
staging:
|
|
183
|
+
file: .env.staging
|
|
184
|
+
production:
|
|
185
|
+
file: .env.production
|
|
186
|
+
requireConfirmation: true
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
## Environment Variables
|
|
190
|
+
|
|
191
|
+
- `SYNCENV_API_URL` - API base URL (default: http://localhost:8787)
|
|
192
|
+
- `SYNCENV_VERBOSE` - Enable verbose logging
|
|
193
|
+
|
|
194
|
+
## Security
|
|
195
|
+
|
|
196
|
+
- All encryption/decryption happens locally on your device
|
|
197
|
+
- Master password never leaves your device
|
|
198
|
+
- Data Encryption Keys (DEK) are encrypted with your Key Encryption Key (KEK)
|
|
199
|
+
- KEK is derived from your master password using Argon2id
|
|
200
|
+
- Server only stores encrypted data and cannot decrypt your environment variables
|
|
201
|
+
|
|
202
|
+
## License
|
|
203
|
+
|
|
204
|
+
MIT
|
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
// src/merge/interactive.ts
|
|
2
|
+
import * as readline from "readline";
|
|
3
|
+
|
|
4
|
+
// src/merge/three-way.ts
|
|
5
|
+
function threeWayMerge(input) {
|
|
6
|
+
const { base, local, remote } = input;
|
|
7
|
+
const allKeys = /* @__PURE__ */ new Set([...Object.keys(base), ...Object.keys(local), ...Object.keys(remote)]);
|
|
8
|
+
const result = {};
|
|
9
|
+
const conflicts = [];
|
|
10
|
+
let added = 0;
|
|
11
|
+
let modified = 0;
|
|
12
|
+
let deleted = 0;
|
|
13
|
+
let unchanged = 0;
|
|
14
|
+
for (const key of allKeys) {
|
|
15
|
+
const baseVal = base[key] ?? null;
|
|
16
|
+
const localVal = local[key] ?? null;
|
|
17
|
+
const remoteVal = remote[key] ?? null;
|
|
18
|
+
if (localVal === remoteVal) {
|
|
19
|
+
if (localVal !== null) {
|
|
20
|
+
result[key] = localVal;
|
|
21
|
+
if (baseVal === null) {
|
|
22
|
+
added++;
|
|
23
|
+
} else if (baseVal !== localVal) {
|
|
24
|
+
modified++;
|
|
25
|
+
} else {
|
|
26
|
+
unchanged++;
|
|
27
|
+
}
|
|
28
|
+
} else if (baseVal !== null) {
|
|
29
|
+
deleted++;
|
|
30
|
+
}
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
if (localVal === baseVal) {
|
|
34
|
+
if (remoteVal !== null) {
|
|
35
|
+
result[key] = remoteVal;
|
|
36
|
+
if (baseVal === null) {
|
|
37
|
+
added++;
|
|
38
|
+
} else {
|
|
39
|
+
modified++;
|
|
40
|
+
}
|
|
41
|
+
} else {
|
|
42
|
+
deleted++;
|
|
43
|
+
}
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
if (remoteVal === baseVal) {
|
|
47
|
+
if (localVal !== null) {
|
|
48
|
+
result[key] = localVal;
|
|
49
|
+
if (baseVal === null) {
|
|
50
|
+
added++;
|
|
51
|
+
} else {
|
|
52
|
+
modified++;
|
|
53
|
+
}
|
|
54
|
+
} else {
|
|
55
|
+
deleted++;
|
|
56
|
+
}
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
conflicts.push({
|
|
60
|
+
key,
|
|
61
|
+
baseValue: baseVal,
|
|
62
|
+
localValue: localVal,
|
|
63
|
+
remoteValue: remoteVal
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
autoMerged: conflicts.length === 0,
|
|
68
|
+
mergedContent: formatEnvFile(result),
|
|
69
|
+
conflicts,
|
|
70
|
+
statistics: {
|
|
71
|
+
added,
|
|
72
|
+
modified,
|
|
73
|
+
deleted,
|
|
74
|
+
conflicts: conflicts.length,
|
|
75
|
+
unchanged
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
function resolveConflicts(base, local, remote, conflicts) {
|
|
80
|
+
const result = {};
|
|
81
|
+
const allKeys = /* @__PURE__ */ new Set([...Object.keys(base), ...Object.keys(local), ...Object.keys(remote)]);
|
|
82
|
+
for (const key of allKeys) {
|
|
83
|
+
const baseVal = base[key] ?? null;
|
|
84
|
+
const localVal = local[key] ?? null;
|
|
85
|
+
const remoteVal = remote[key] ?? null;
|
|
86
|
+
const conflict = conflicts.find((c) => c.key === key);
|
|
87
|
+
if (conflict) {
|
|
88
|
+
switch (conflict.resolution) {
|
|
89
|
+
case "local":
|
|
90
|
+
if (localVal !== null) result[key] = localVal;
|
|
91
|
+
break;
|
|
92
|
+
case "remote":
|
|
93
|
+
if (remoteVal !== null) result[key] = remoteVal;
|
|
94
|
+
break;
|
|
95
|
+
case "custom":
|
|
96
|
+
if (conflict.customValue !== void 0 && conflict.customValue !== null) {
|
|
97
|
+
result[key] = conflict.customValue;
|
|
98
|
+
}
|
|
99
|
+
break;
|
|
100
|
+
default:
|
|
101
|
+
break;
|
|
102
|
+
}
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
if (localVal === remoteVal) {
|
|
106
|
+
if (localVal !== null) result[key] = localVal;
|
|
107
|
+
} else if (localVal === baseVal) {
|
|
108
|
+
if (remoteVal !== null) result[key] = remoteVal;
|
|
109
|
+
} else {
|
|
110
|
+
if (localVal !== null) result[key] = localVal;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
const unresolvedConflicts = conflicts.filter((c) => !c.resolution);
|
|
114
|
+
if (unresolvedConflicts.length > 0) {
|
|
115
|
+
return formatEnvFileWithConflicts(result, unresolvedConflicts);
|
|
116
|
+
}
|
|
117
|
+
return formatEnvFile(result);
|
|
118
|
+
}
|
|
119
|
+
function formatEnvFile(vars) {
|
|
120
|
+
const lines = [];
|
|
121
|
+
for (const [key, value] of Object.entries(vars)) {
|
|
122
|
+
if (value.includes("\n") || value.includes('"')) {
|
|
123
|
+
if (!value.includes("'")) {
|
|
124
|
+
lines.push(`${key}='${value}'`);
|
|
125
|
+
} else {
|
|
126
|
+
lines.push(`${key}="${value.replace(/"/g, '\\"')}"`);
|
|
127
|
+
}
|
|
128
|
+
} else if (value.includes(" ") || value.includes("#")) {
|
|
129
|
+
lines.push(`${key}="${value}"`);
|
|
130
|
+
} else {
|
|
131
|
+
lines.push(`${key}=${value}`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return lines.join("\n") + (lines.length > 0 ? "\n" : "");
|
|
135
|
+
}
|
|
136
|
+
function formatEnvFileWithConflicts(vars, conflicts) {
|
|
137
|
+
const lines = ["# WARNING: This file contains unresolved merge conflicts", ""];
|
|
138
|
+
for (const [key, value] of Object.entries(vars)) {
|
|
139
|
+
const conflict = conflicts.find((c) => c.key === key);
|
|
140
|
+
if (conflict) {
|
|
141
|
+
lines.push(`# CONFLICT: ${key}`);
|
|
142
|
+
lines.push(`# BASE: ${conflict.baseValue ?? "(deleted)"}`);
|
|
143
|
+
lines.push(`${key}=<<<<<<< LOCAL`);
|
|
144
|
+
lines.push(conflict.localValue ?? "");
|
|
145
|
+
lines.push("=======");
|
|
146
|
+
lines.push(conflict.remoteValue ?? "");
|
|
147
|
+
lines.push(">>>>>>> REMOTE");
|
|
148
|
+
lines.push("");
|
|
149
|
+
} else {
|
|
150
|
+
lines.push(`${key}=${value}`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return lines.join("\n") + "\n";
|
|
154
|
+
}
|
|
155
|
+
function applyMergeStrategy(input, strategy) {
|
|
156
|
+
const result = threeWayMerge(input);
|
|
157
|
+
if (result.autoMerged) {
|
|
158
|
+
return result;
|
|
159
|
+
}
|
|
160
|
+
if (strategy === "fail-on-conflict") {
|
|
161
|
+
throw new Error(
|
|
162
|
+
`Merge failed: ${result.conflicts.length} conflict(s) found. Keys: ${result.conflicts.map((c) => c.key).join(", ")}`
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
const resolvedConflicts = result.conflicts.map((c) => ({
|
|
166
|
+
...c,
|
|
167
|
+
resolution: strategy === "local-wins" ? "local" : "remote"
|
|
168
|
+
}));
|
|
169
|
+
const resolvedContent = resolveConflicts(
|
|
170
|
+
input.base,
|
|
171
|
+
input.local,
|
|
172
|
+
input.remote,
|
|
173
|
+
resolvedConflicts
|
|
174
|
+
);
|
|
175
|
+
return {
|
|
176
|
+
autoMerged: true,
|
|
177
|
+
mergedContent: resolvedContent,
|
|
178
|
+
conflicts: resolvedConflicts,
|
|
179
|
+
statistics: {
|
|
180
|
+
...result.statistics,
|
|
181
|
+
conflicts: 0
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// src/merge/interactive.ts
|
|
187
|
+
var COLORS = {
|
|
188
|
+
reset: "\x1B[0m",
|
|
189
|
+
red: "\x1B[31m",
|
|
190
|
+
green: "\x1B[32m",
|
|
191
|
+
yellow: "\x1B[33m",
|
|
192
|
+
blue: "\x1B[34m",
|
|
193
|
+
magenta: "\x1B[35m",
|
|
194
|
+
cyan: "\x1B[36m",
|
|
195
|
+
gray: "\x1B[90m"
|
|
196
|
+
};
|
|
197
|
+
async function interactiveMerge(input, mergeResult, options = {}) {
|
|
198
|
+
const { base, local, remote } = input;
|
|
199
|
+
const { conflicts, statistics } = mergeResult;
|
|
200
|
+
printMergeSummary(statistics);
|
|
201
|
+
const resolvedConflicts = [];
|
|
202
|
+
for (const conflict of conflicts) {
|
|
203
|
+
printConflictDetails(conflict);
|
|
204
|
+
const choice = await promptForResolution(conflict, options);
|
|
205
|
+
if (choice.type === "cancel") {
|
|
206
|
+
return { success: false, content: "", resolvedConflicts, cancelled: true };
|
|
207
|
+
}
|
|
208
|
+
if (choice.type === "save-draft") {
|
|
209
|
+
resolvedConflicts.push(
|
|
210
|
+
...conflicts.slice(resolvedConflicts.length).map((c) => ({
|
|
211
|
+
...c,
|
|
212
|
+
resolution: void 0
|
|
213
|
+
}))
|
|
214
|
+
);
|
|
215
|
+
const content2 = resolveConflicts(base, local, remote, resolvedConflicts);
|
|
216
|
+
return { success: true, content: content2, resolvedConflicts, cancelled: false };
|
|
217
|
+
}
|
|
218
|
+
resolvedConflicts.push({
|
|
219
|
+
...conflict,
|
|
220
|
+
resolution: choice.resolution,
|
|
221
|
+
customValue: choice.customValue
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
const content = resolveConflicts(base, local, remote, resolvedConflicts);
|
|
225
|
+
const confirmed = await confirmFinalMerge(mergeResult.statistics);
|
|
226
|
+
if (!confirmed) {
|
|
227
|
+
return { success: false, content: "", resolvedConflicts, cancelled: true };
|
|
228
|
+
}
|
|
229
|
+
return { success: true, content, resolvedConflicts, cancelled: false };
|
|
230
|
+
}
|
|
231
|
+
function printMergeSummary(stats) {
|
|
232
|
+
console.log("");
|
|
233
|
+
console.log(
|
|
234
|
+
`${COLORS.yellow}\u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510${COLORS.reset}`
|
|
235
|
+
);
|
|
236
|
+
console.log(
|
|
237
|
+
`${COLORS.yellow}\u2502${COLORS.reset} ${COLORS.yellow}\u26A0\uFE0F Conflicts detected, manual resolution required${COLORS.reset} ${COLORS.yellow}\u2502${COLORS.reset}`
|
|
238
|
+
);
|
|
239
|
+
console.log(
|
|
240
|
+
`${COLORS.yellow}\u251C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524${COLORS.reset}`
|
|
241
|
+
);
|
|
242
|
+
if (stats.added > 0) {
|
|
243
|
+
console.log(
|
|
244
|
+
`${COLORS.yellow}\u2502${COLORS.reset} ${COLORS.green}Added:${COLORS.reset} ${stats.added.toString().padStart(3)}${" ".repeat(42)}${COLORS.yellow}\u2502${COLORS.reset}`
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
if (stats.modified > 0) {
|
|
248
|
+
console.log(
|
|
249
|
+
`${COLORS.yellow}\u2502${COLORS.reset} ${COLORS.cyan}Modified:${COLORS.reset} ${stats.modified.toString().padStart(3)}${" ".repeat(39)}${COLORS.yellow}\u2502${COLORS.reset}`
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
if (stats.deleted > 0) {
|
|
253
|
+
console.log(
|
|
254
|
+
`${COLORS.yellow}\u2502${COLORS.reset} ${COLORS.red}Deleted:${COLORS.reset} ${stats.deleted.toString().padStart(3)}${" ".repeat(40)}${COLORS.yellow}\u2502${COLORS.reset}`
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
console.log(
|
|
258
|
+
`${COLORS.yellow}\u2502${COLORS.reset} ${COLORS.red}\u2715 Conflicts: ${stats.conflicts}${" ".repeat(35)}${COLORS.yellow}\u2502${COLORS.reset}`
|
|
259
|
+
);
|
|
260
|
+
console.log(
|
|
261
|
+
`${COLORS.yellow}\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518${COLORS.reset}`
|
|
262
|
+
);
|
|
263
|
+
console.log("");
|
|
264
|
+
}
|
|
265
|
+
function printConflictDetails(conflict) {
|
|
266
|
+
const { key, baseValue, localValue, remoteValue } = conflict;
|
|
267
|
+
console.log(`${COLORS.magenta}Conflict: ${key}${COLORS.reset}`);
|
|
268
|
+
console.log(
|
|
269
|
+
`${COLORS.gray}\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500${COLORS.reset}`
|
|
270
|
+
);
|
|
271
|
+
if (baseValue !== null) {
|
|
272
|
+
console.log(`${COLORS.gray}Base value:${COLORS.reset}`);
|
|
273
|
+
console.log(` ${truncate(baseValue, 60)}`);
|
|
274
|
+
} else {
|
|
275
|
+
console.log(`${COLORS.gray}Base: (new variable)${COLORS.reset}`);
|
|
276
|
+
}
|
|
277
|
+
console.log("");
|
|
278
|
+
if (localValue !== null) {
|
|
279
|
+
console.log(`${COLORS.green}[L] Local:${COLORS.reset}`);
|
|
280
|
+
console.log(` ${truncate(localValue, 60)}`);
|
|
281
|
+
} else {
|
|
282
|
+
console.log(`${COLORS.green}[L] Local: (deleted)${COLORS.reset}`);
|
|
283
|
+
}
|
|
284
|
+
console.log("");
|
|
285
|
+
if (remoteValue !== null) {
|
|
286
|
+
console.log(`${COLORS.blue}[R] Remote:${COLORS.reset}`);
|
|
287
|
+
console.log(` ${truncate(remoteValue, 60)}`);
|
|
288
|
+
} else {
|
|
289
|
+
console.log(`${COLORS.blue}[R] Remote: (deleted)${COLORS.reset}`);
|
|
290
|
+
}
|
|
291
|
+
console.log("");
|
|
292
|
+
}
|
|
293
|
+
async function promptForResolution(conflict, options) {
|
|
294
|
+
const rl = readline.createInterface({
|
|
295
|
+
input: process.stdin,
|
|
296
|
+
output: process.stdout
|
|
297
|
+
});
|
|
298
|
+
const ask = (prompt) => {
|
|
299
|
+
return new Promise((resolve) => {
|
|
300
|
+
rl.question(prompt, resolve);
|
|
301
|
+
});
|
|
302
|
+
};
|
|
303
|
+
try {
|
|
304
|
+
while (true) {
|
|
305
|
+
const choices = [];
|
|
306
|
+
if (conflict.localValue !== null) {
|
|
307
|
+
choices.push("[l] Use local value (Local)");
|
|
308
|
+
}
|
|
309
|
+
if (conflict.remoteValue !== null) {
|
|
310
|
+
choices.push("[r] Use remote value (Remote)");
|
|
311
|
+
}
|
|
312
|
+
if (options.allowBoth && conflict.localValue !== null && conflict.remoteValue !== null) {
|
|
313
|
+
choices.push("[b] Keep both (Both)");
|
|
314
|
+
}
|
|
315
|
+
if (options.allowEdit) {
|
|
316
|
+
choices.push("[e] Edit custom value (Edit)");
|
|
317
|
+
}
|
|
318
|
+
choices.push("[s] Skip, keep conflict markers (Skip)");
|
|
319
|
+
choices.push("[d] Save draft, continue later (Draft)");
|
|
320
|
+
choices.push("[q] Cancel merge (Cancel)");
|
|
321
|
+
console.log("How would you like to resolve this conflict?");
|
|
322
|
+
choices.forEach((c) => console.log(` ${c}`));
|
|
323
|
+
console.log("");
|
|
324
|
+
const answer = await ask("Choice: ");
|
|
325
|
+
const choice = answer.trim().toLowerCase();
|
|
326
|
+
switch (choice) {
|
|
327
|
+
case "l":
|
|
328
|
+
case "local":
|
|
329
|
+
if (conflict.localValue !== null) {
|
|
330
|
+
return { type: "resolve", resolution: "local" };
|
|
331
|
+
}
|
|
332
|
+
console.log(`${COLORS.red}Local value does not exist, cannot select${COLORS.reset}`);
|
|
333
|
+
break;
|
|
334
|
+
case "r":
|
|
335
|
+
case "remote":
|
|
336
|
+
if (conflict.remoteValue !== null) {
|
|
337
|
+
return { type: "resolve", resolution: "remote" };
|
|
338
|
+
}
|
|
339
|
+
console.log(`${COLORS.red}Remote value does not exist, cannot select${COLORS.reset}`);
|
|
340
|
+
break;
|
|
341
|
+
case "b":
|
|
342
|
+
case "both":
|
|
343
|
+
if (conflict.localValue !== null && conflict.remoteValue !== null) {
|
|
344
|
+
const customValue = `${conflict.localValue}
|
|
345
|
+
# MERGED_FROM_REMOTE: ${conflict.remoteValue}`;
|
|
346
|
+
return { type: "resolve", resolution: "custom", customValue };
|
|
347
|
+
}
|
|
348
|
+
console.log(`${COLORS.red}Both values must exist to select this option${COLORS.reset}`);
|
|
349
|
+
break;
|
|
350
|
+
case "e":
|
|
351
|
+
case "edit":
|
|
352
|
+
if (options.allowEdit) {
|
|
353
|
+
const customValue = await ask("Enter custom value: ");
|
|
354
|
+
return { type: "resolve", resolution: "local", customValue };
|
|
355
|
+
}
|
|
356
|
+
break;
|
|
357
|
+
case "s":
|
|
358
|
+
case "skip":
|
|
359
|
+
return { type: "resolve", resolution: "local" };
|
|
360
|
+
case "d":
|
|
361
|
+
case "draft":
|
|
362
|
+
return { type: "save-draft" };
|
|
363
|
+
case "q":
|
|
364
|
+
case "quit":
|
|
365
|
+
case "cancel":
|
|
366
|
+
return { type: "cancel" };
|
|
367
|
+
default:
|
|
368
|
+
console.log(`${COLORS.red}Invalid option, please select again${COLORS.reset}`);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
} finally {
|
|
372
|
+
rl.close();
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
async function confirmFinalMerge(stats) {
|
|
376
|
+
const rl = readline.createInterface({
|
|
377
|
+
input: process.stdin,
|
|
378
|
+
output: process.stdout
|
|
379
|
+
});
|
|
380
|
+
return new Promise((resolve) => {
|
|
381
|
+
console.log("");
|
|
382
|
+
console.log(`${COLORS.cyan}Merge Summary:${COLORS.reset}`);
|
|
383
|
+
console.log(` Added: ${stats.added}`);
|
|
384
|
+
console.log(` Modified: ${stats.modified}`);
|
|
385
|
+
console.log(` Deleted: ${stats.deleted}`);
|
|
386
|
+
console.log(` Conflicts resolved: ${stats.conflicts}`);
|
|
387
|
+
console.log("");
|
|
388
|
+
rl.question("Apply this merge? [y/N]: ", (answer) => {
|
|
389
|
+
rl.close();
|
|
390
|
+
resolve(answer.trim().toLowerCase() === "y");
|
|
391
|
+
});
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
function truncate(str, maxLength) {
|
|
395
|
+
if (str.length <= maxLength) return str;
|
|
396
|
+
return str.substring(0, maxLength - 3) + "...";
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
export {
|
|
400
|
+
threeWayMerge,
|
|
401
|
+
applyMergeStrategy,
|
|
402
|
+
interactiveMerge
|
|
403
|
+
};
|