@magentrix-corp/magentrix-cli 1.3.16 → 1.3.17
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/LICENSE +25 -25
- package/README.md +1166 -1166
- package/actions/autopublish.old.js +293 -293
- package/actions/config.js +182 -182
- package/actions/create.js +466 -466
- package/actions/help.js +164 -164
- package/actions/iris/buildStage.js +874 -874
- package/actions/iris/delete.js +256 -256
- package/actions/iris/dev.js +391 -391
- package/actions/iris/index.js +6 -6
- package/actions/iris/link.js +375 -375
- package/actions/iris/recover.js +268 -268
- package/actions/main.js +80 -80
- package/actions/publish.js +1420 -1420
- package/actions/pull.js +684 -684
- package/actions/setup.js +148 -148
- package/actions/status.js +17 -17
- package/actions/update.js +248 -248
- package/bin/magentrix.js +393 -393
- package/package.json +55 -55
- package/utils/assetPaths.js +158 -158
- package/utils/autopublishLock.js +77 -77
- package/utils/cacher.js +206 -206
- package/utils/cli/checkInstanceUrl.js +76 -74
- package/utils/cli/helpers/compare.js +282 -282
- package/utils/cli/helpers/ensureApiKey.js +63 -63
- package/utils/cli/helpers/ensureCredentials.js +68 -68
- package/utils/cli/helpers/ensureInstanceUrl.js +75 -75
- package/utils/cli/writeRecords.js +262 -262
- package/utils/compare.js +135 -135
- package/utils/compress.js +17 -17
- package/utils/config.js +527 -527
- package/utils/debug.js +144 -144
- package/utils/diagnostics/testPublishLogic.js +96 -96
- package/utils/diff.js +49 -49
- package/utils/downloadAssets.js +291 -291
- package/utils/filetag.js +115 -115
- package/utils/hash.js +14 -14
- package/utils/iris/backup.js +411 -411
- package/utils/iris/builder.js +541 -541
- package/utils/iris/config-reader.js +664 -664
- package/utils/iris/deleteHelper.js +150 -150
- package/utils/iris/errors.js +537 -537
- package/utils/iris/linker.js +601 -601
- package/utils/iris/lock.js +360 -360
- package/utils/iris/validation.js +360 -360
- package/utils/iris/validator.js +281 -281
- package/utils/iris/zipper.js +248 -248
- package/utils/logger.js +291 -291
- package/utils/magentrix/api/assets.js +220 -220
- package/utils/magentrix/api/auth.js +107 -107
- package/utils/magentrix/api/createEntity.js +61 -61
- package/utils/magentrix/api/deleteEntity.js +55 -55
- package/utils/magentrix/api/iris.js +251 -251
- package/utils/magentrix/api/meqlQuery.js +36 -36
- package/utils/magentrix/api/retrieveEntity.js +86 -86
- package/utils/magentrix/api/updateEntity.js +66 -66
- package/utils/magentrix/fetch.js +168 -168
- package/utils/merge.js +22 -22
- package/utils/permissionError.js +70 -70
- package/utils/preferences.js +40 -40
- package/utils/progress.js +469 -469
- package/utils/spinner.js +43 -43
- package/utils/template.js +52 -52
- package/utils/updateFileBase.js +121 -121
- package/utils/workspaces.js +108 -108
- package/vars/config.js +11 -11
- package/vars/global.js +50 -50
|
@@ -1,262 +1,262 @@
|
|
|
1
|
-
import { EXPORT_ROOT, TYPE_DIR_MAP } from "../../vars/global.js";
|
|
2
|
-
import fs from 'fs';
|
|
3
|
-
import path from "path";
|
|
4
|
-
import { compareLocalAndRemote } from "../compare.js";
|
|
5
|
-
import { mergeFiles } from "../merge.js";
|
|
6
|
-
import Config from "../config.js";
|
|
7
|
-
import chalk from "chalk";
|
|
8
|
-
import { diffLines } from 'diff';
|
|
9
|
-
import readlineSync from "readline-sync";
|
|
10
|
-
import { updateBase } from "../updateFileBase.js";
|
|
11
|
-
import { sha256 } from "../hash.js";
|
|
12
|
-
import { openDiffInVSCode } from "../diff.js";
|
|
13
|
-
import { tmpdir } from "os";
|
|
14
|
-
import { v4 as uuidv4 } from 'uuid';
|
|
15
|
-
import { findFileByTag, getFileTag, setFileTag } from "../filetag.js";
|
|
16
|
-
import { decompressString } from "../compress.js";
|
|
17
|
-
|
|
18
|
-
const config = new Config();
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Maps an ActivePage record to its local file representation.
|
|
22
|
-
* @param {Object} record - The ActivePage record from the API.
|
|
23
|
-
* @returns {Object} The enriched record with 'relativePath' property.
|
|
24
|
-
*/
|
|
25
|
-
export const mapRecordToFile = (record) => {
|
|
26
|
-
const { Type, Name, Id } = record;
|
|
27
|
-
const mapping = TYPE_DIR_MAP[Type];
|
|
28
|
-
|
|
29
|
-
if (!mapping) {
|
|
30
|
-
const errMsg = `Unrecognized type "${Type}" for record "${Name || 'UNKNOWN'}". This can be resolved in the Magentrix IDE and is likely due to a misconfiguration.`;
|
|
31
|
-
return { ...record, error: errMsg };
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const safeName = Name.replace(/[<>:"/\\|?*]+/g, "_");
|
|
35
|
-
const filename = `${safeName}.${mapping.extension}`;
|
|
36
|
-
|
|
37
|
-
const foundPathByRecordId = findFileByTag(Id) || "";
|
|
38
|
-
const relativeFoundPath = foundPathByRecordId
|
|
39
|
-
? path.join(mapping.directory, foundPathByRecordId.split(mapping.directory)[1] || "")
|
|
40
|
-
: "";
|
|
41
|
-
|
|
42
|
-
// Fix for Bug 1: If the file exists but the name doesn't match the record name,
|
|
43
|
-
// we should treat it as a rename and use the new name.
|
|
44
|
-
let useExistingPath = false;
|
|
45
|
-
if (foundPathByRecordId) {
|
|
46
|
-
const existingName = path.basename(foundPathByRecordId);
|
|
47
|
-
// Check if existing name matches the expected filename (ignoring extension case if needed, but strict for now)
|
|
48
|
-
if (existingName === filename) {
|
|
49
|
-
useExistingPath = true;
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
const filePath = useExistingPath ? relativeFoundPath : path.join(mapping.directory, filename);
|
|
54
|
-
|
|
55
|
-
return {
|
|
56
|
-
...record,
|
|
57
|
-
relativePath: filePath
|
|
58
|
-
};
|
|
59
|
-
};
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Writes records to disk, handling conflicts per the chosen resolution method.
|
|
63
|
-
* @param {Array} records - List of entity records (ActiveClass or ActivePage)
|
|
64
|
-
* @param {string} resolutionMethod - One of: "overwrite", "merge", "diff", "skip"
|
|
65
|
-
* @param {Object} progress - Optional progress tracker
|
|
66
|
-
* @param {Object} logger - Optional logger for warnings/errors
|
|
67
|
-
*/
|
|
68
|
-
export const writeRecords = async (records, resolutionMethod, progress = null, logger = null) => {
|
|
69
|
-
for (let i = 0; i < records.length; i++) {
|
|
70
|
-
const record = records[i];
|
|
71
|
-
const mapping = TYPE_DIR_MAP[record.Type];
|
|
72
|
-
if (!mapping) {
|
|
73
|
-
const msg = `Skipping unknown type: ${record.Type} (record: ${record.Name})`;
|
|
74
|
-
if (logger) {
|
|
75
|
-
logger.warning(msg);
|
|
76
|
-
} else {
|
|
77
|
-
console.warn(chalk.gray(`⚠️ ${msg}`));
|
|
78
|
-
}
|
|
79
|
-
continue;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// Build output path and filename
|
|
83
|
-
const outputDir = path.join(EXPORT_ROOT, mapping.directory);
|
|
84
|
-
fs.mkdirSync(outputDir, { recursive: true }); // Ensure dir exists
|
|
85
|
-
|
|
86
|
-
const safeName = record.Name.replace(/[<>:"/\\|?*]+/g, "_");
|
|
87
|
-
const filename = `${safeName}.${mapping.extension}`;
|
|
88
|
-
const filePath = path.join(outputDir, filename);
|
|
89
|
-
|
|
90
|
-
const content = record.Content;
|
|
91
|
-
if (!content) {
|
|
92
|
-
const msg = `No content found for ${record.Name} (${record.Type}), skipping file.`;
|
|
93
|
-
if (logger) {
|
|
94
|
-
logger.warning(msg);
|
|
95
|
-
} else {
|
|
96
|
-
console.warn(`⚠️ ${msg}`);
|
|
97
|
-
}
|
|
98
|
-
continue;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// Compare local and remote to determine sync status/conflict
|
|
102
|
-
const comparison = compareLocalAndRemote(filePath, { content, ...record });
|
|
103
|
-
|
|
104
|
-
// --- Conflict Handling ---
|
|
105
|
-
let finalContent = null; // Will hold the string to write if we should write the file
|
|
106
|
-
|
|
107
|
-
if (['missing', 'in_sync'].includes(comparison.status)) {
|
|
108
|
-
// No conflict, just write server version (create if missing)
|
|
109
|
-
finalContent = content;
|
|
110
|
-
} else {
|
|
111
|
-
// Attempt to read local file (may not exist)
|
|
112
|
-
let local = "";
|
|
113
|
-
|
|
114
|
-
const base = config.read(record.Id, { global: false, filename: 'base.json' });
|
|
115
|
-
const baseContent = decompressString(base?.compressedContent) || '';
|
|
116
|
-
|
|
117
|
-
// const base = config.read(filePath);
|
|
118
|
-
try { local = fs.readFileSync(filePath, "utf8"); } catch { }
|
|
119
|
-
|
|
120
|
-
if (resolutionMethod === 'skip') {
|
|
121
|
-
// Do nothing, never create or update file
|
|
122
|
-
console.log(chalk.yellow(`⏭️ Skipped ${record.Name} (${record.Type}) due to conflict or mismatch.`));
|
|
123
|
-
continue;
|
|
124
|
-
}
|
|
125
|
-
else if (resolutionMethod === 'overwrite') {
|
|
126
|
-
// Always accept server version, creating file if missing
|
|
127
|
-
console.log(chalk.redBright(`⚠️ Overwriting local file: ${filePath} with server version.`));
|
|
128
|
-
finalContent = content;
|
|
129
|
-
}
|
|
130
|
-
else if (resolutionMethod === 'diff') {
|
|
131
|
-
// Use mergeFiles, show diff, ask user
|
|
132
|
-
let merged;
|
|
133
|
-
try {
|
|
134
|
-
merged = mergeFiles(baseContent, local, content);
|
|
135
|
-
} catch (e) {
|
|
136
|
-
console.log(chalk.red(`❌ Error merging: ${e.message}`));
|
|
137
|
-
continue;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
// Attempt to open the diff in vs code
|
|
141
|
-
const randomStr = (length) => Math.random().toString(36).slice(2, 2 + length);
|
|
142
|
-
const tempFileServer = path.join(tmpdir(), `${randomStr(4)}.Server${filename}`);
|
|
143
|
-
const tempFileLocal = path.join(tmpdir(), `${randomStr(4)}.Local${filename}`);
|
|
144
|
-
fs.writeFileSync(tempFileServer, content);
|
|
145
|
-
fs.writeFileSync(tempFileLocal, local);
|
|
146
|
-
|
|
147
|
-
const vsCodeDiffDisplayed = openDiffInVSCode(tempFileServer, tempFileLocal);
|
|
148
|
-
|
|
149
|
-
// If the VS Code diff failed, show in terminal
|
|
150
|
-
if (!vsCodeDiffDisplayed) {
|
|
151
|
-
// Show diff (local vs merged)
|
|
152
|
-
const diff = diffLines(local, merged);
|
|
153
|
-
console.log(chalk.magenta.bold(`\n======= REVIEW MERGED for ${filename} =======`));
|
|
154
|
-
diff.forEach(part => {
|
|
155
|
-
const color = part.added ? 'green' :
|
|
156
|
-
part.removed ? 'red' : 'gray';
|
|
157
|
-
process.stdout.write(chalk[color](part.value));
|
|
158
|
-
});
|
|
159
|
-
console.log(chalk.magenta.bold("\n======= END REVIEW ======="));
|
|
160
|
-
|
|
161
|
-
// Warn if there are still unresolved conflict markers
|
|
162
|
-
if (merged.includes('<<<<<<<')) {
|
|
163
|
-
console.log(chalk.red.bold("\n⚠️ Merge conflict markers present. Please resolve manually after accepting."));
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
// Prompt user to accept or skip writing merged content
|
|
168
|
-
const choice = readlineSync.question(
|
|
169
|
-
chalk.yellow("\nAccept merged changes and write to file? (y/n): ")
|
|
170
|
-
);
|
|
171
|
-
|
|
172
|
-
// Delete the temp files
|
|
173
|
-
fs.unlinkSync(tempFileLocal);
|
|
174
|
-
fs.unlinkSync(tempFileServer);
|
|
175
|
-
|
|
176
|
-
if (choice.trim().toLowerCase() === 'y') {
|
|
177
|
-
finalContent = merged;
|
|
178
|
-
} else {
|
|
179
|
-
console.log(chalk.yellow("⏭️ Skipped due to user choice."));
|
|
180
|
-
continue;
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
else if (resolutionMethod === 'merge') {
|
|
184
|
-
// No prompt: always write merged result (may contain conflict markers)
|
|
185
|
-
let merged;
|
|
186
|
-
|
|
187
|
-
try {
|
|
188
|
-
merged = mergeFiles(baseContent, local, content);
|
|
189
|
-
} catch (e) {
|
|
190
|
-
console.log(chalk.red(`❌ Error merging: ${e.message}`));
|
|
191
|
-
continue;
|
|
192
|
-
}
|
|
193
|
-
if (merged.includes('<<<<<<<')) {
|
|
194
|
-
console.log(chalk.red.bold(`⚠️ Merge conflict markers written to ${filename}. Manual review required.`));
|
|
195
|
-
} else {
|
|
196
|
-
console.log(chalk.green(`✔️ Merged ${filename} automatically.`));
|
|
197
|
-
}
|
|
198
|
-
finalContent = merged;
|
|
199
|
-
}
|
|
200
|
-
else {
|
|
201
|
-
// Unknown or unsupported mode (should not occur)
|
|
202
|
-
console.log(chalk.red(`❌ Unknown resolution method: ${resolutionMethod}`));
|
|
203
|
-
continue;
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
// --- Actually Write File (if required by resolution) ---
|
|
208
|
-
if (finalContent !== null) {
|
|
209
|
-
try {
|
|
210
|
-
// Lookup a matching file dir in our cache, in case the file was renamed
|
|
211
|
-
const cachedFilePath = findFileByTag(record.Id) || path.resolve(filePath);
|
|
212
|
-
|
|
213
|
-
// Ensure the directory exists (paranoia for deeply nested files)
|
|
214
|
-
fs.mkdirSync(path.dirname(cachedFilePath), { recursive: true });
|
|
215
|
-
fs.writeFileSync(cachedFilePath, finalContent, "utf8");
|
|
216
|
-
|
|
217
|
-
// Add a file tag so we can keep track of file changes
|
|
218
|
-
await setFileTag(cachedFilePath, record.Id);
|
|
219
|
-
|
|
220
|
-
// Set mtime/atime for reproducible syncs (optional, best effort)
|
|
221
|
-
let mtimeMs = null;
|
|
222
|
-
if (record.ModifiedOn) {
|
|
223
|
-
mtimeMs = new Date(record.ModifiedOn).getTime();
|
|
224
|
-
} else if (record.CreatedOn) {
|
|
225
|
-
mtimeMs = new Date(record.CreatedOn).getTime();
|
|
226
|
-
}
|
|
227
|
-
if (mtimeMs) {
|
|
228
|
-
const mtimeSeconds = mtimeMs / 1000;
|
|
229
|
-
try {
|
|
230
|
-
fs.utimesSync(cachedFilePath, mtimeSeconds, mtimeSeconds);
|
|
231
|
-
} catch (e) {
|
|
232
|
-
const msg = `Could not set times for ${cachedFilePath}: ${e.message}`;
|
|
233
|
-
if (logger) {
|
|
234
|
-
logger.warning(msg);
|
|
235
|
-
} else {
|
|
236
|
-
console.warn(`⚠️ ${msg}`);
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
// If the server records content and the local files content match then we can update the base
|
|
243
|
-
if (sha256(finalContent) === sha256(record.Content)) {
|
|
244
|
-
// We still need to use the expected filePath as the rename might be local only
|
|
245
|
-
updateBase(filePath, record, cachedFilePath);
|
|
246
|
-
}
|
|
247
|
-
} catch (err) {
|
|
248
|
-
const msg = `Failed to write file ${filePath}: ${err.message}`;
|
|
249
|
-
if (logger) {
|
|
250
|
-
logger.error(msg, err);
|
|
251
|
-
} else {
|
|
252
|
-
console.error(chalk.red(`❌ ${msg}`));
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
// Update progress (outside try-catch so it always updates)
|
|
258
|
-
if (progress && (i % 10 === 0 || i === records.length - 1)) {
|
|
259
|
-
progress.updateProgress('write', i + 1, records.length, `Writing files...`);
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
};
|
|
1
|
+
import { EXPORT_ROOT, TYPE_DIR_MAP } from "../../vars/global.js";
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { compareLocalAndRemote } from "../compare.js";
|
|
5
|
+
import { mergeFiles } from "../merge.js";
|
|
6
|
+
import Config from "../config.js";
|
|
7
|
+
import chalk from "chalk";
|
|
8
|
+
import { diffLines } from 'diff';
|
|
9
|
+
import readlineSync from "readline-sync";
|
|
10
|
+
import { updateBase } from "../updateFileBase.js";
|
|
11
|
+
import { sha256 } from "../hash.js";
|
|
12
|
+
import { openDiffInVSCode } from "../diff.js";
|
|
13
|
+
import { tmpdir } from "os";
|
|
14
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
15
|
+
import { findFileByTag, getFileTag, setFileTag } from "../filetag.js";
|
|
16
|
+
import { decompressString } from "../compress.js";
|
|
17
|
+
|
|
18
|
+
const config = new Config();
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Maps an ActivePage record to its local file representation.
|
|
22
|
+
* @param {Object} record - The ActivePage record from the API.
|
|
23
|
+
* @returns {Object} The enriched record with 'relativePath' property.
|
|
24
|
+
*/
|
|
25
|
+
export const mapRecordToFile = (record) => {
|
|
26
|
+
const { Type, Name, Id } = record;
|
|
27
|
+
const mapping = TYPE_DIR_MAP[Type];
|
|
28
|
+
|
|
29
|
+
if (!mapping) {
|
|
30
|
+
const errMsg = `Unrecognized type "${Type}" for record "${Name || 'UNKNOWN'}". This can be resolved in the Magentrix IDE and is likely due to a misconfiguration.`;
|
|
31
|
+
return { ...record, error: errMsg };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const safeName = Name.replace(/[<>:"/\\|?*]+/g, "_");
|
|
35
|
+
const filename = `${safeName}.${mapping.extension}`;
|
|
36
|
+
|
|
37
|
+
const foundPathByRecordId = findFileByTag(Id) || "";
|
|
38
|
+
const relativeFoundPath = foundPathByRecordId
|
|
39
|
+
? path.join(mapping.directory, foundPathByRecordId.split(mapping.directory)[1] || "")
|
|
40
|
+
: "";
|
|
41
|
+
|
|
42
|
+
// Fix for Bug 1: If the file exists but the name doesn't match the record name,
|
|
43
|
+
// we should treat it as a rename and use the new name.
|
|
44
|
+
let useExistingPath = false;
|
|
45
|
+
if (foundPathByRecordId) {
|
|
46
|
+
const existingName = path.basename(foundPathByRecordId);
|
|
47
|
+
// Check if existing name matches the expected filename (ignoring extension case if needed, but strict for now)
|
|
48
|
+
if (existingName === filename) {
|
|
49
|
+
useExistingPath = true;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const filePath = useExistingPath ? relativeFoundPath : path.join(mapping.directory, filename);
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
...record,
|
|
57
|
+
relativePath: filePath
|
|
58
|
+
};
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Writes records to disk, handling conflicts per the chosen resolution method.
|
|
63
|
+
* @param {Array} records - List of entity records (ActiveClass or ActivePage)
|
|
64
|
+
* @param {string} resolutionMethod - One of: "overwrite", "merge", "diff", "skip"
|
|
65
|
+
* @param {Object} progress - Optional progress tracker
|
|
66
|
+
* @param {Object} logger - Optional logger for warnings/errors
|
|
67
|
+
*/
|
|
68
|
+
export const writeRecords = async (records, resolutionMethod, progress = null, logger = null) => {
|
|
69
|
+
for (let i = 0; i < records.length; i++) {
|
|
70
|
+
const record = records[i];
|
|
71
|
+
const mapping = TYPE_DIR_MAP[record.Type];
|
|
72
|
+
if (!mapping) {
|
|
73
|
+
const msg = `Skipping unknown type: ${record.Type} (record: ${record.Name})`;
|
|
74
|
+
if (logger) {
|
|
75
|
+
logger.warning(msg);
|
|
76
|
+
} else {
|
|
77
|
+
console.warn(chalk.gray(`⚠️ ${msg}`));
|
|
78
|
+
}
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Build output path and filename
|
|
83
|
+
const outputDir = path.join(EXPORT_ROOT, mapping.directory);
|
|
84
|
+
fs.mkdirSync(outputDir, { recursive: true }); // Ensure dir exists
|
|
85
|
+
|
|
86
|
+
const safeName = record.Name.replace(/[<>:"/\\|?*]+/g, "_");
|
|
87
|
+
const filename = `${safeName}.${mapping.extension}`;
|
|
88
|
+
const filePath = path.join(outputDir, filename);
|
|
89
|
+
|
|
90
|
+
const content = record.Content;
|
|
91
|
+
if (!content) {
|
|
92
|
+
const msg = `No content found for ${record.Name} (${record.Type}), skipping file.`;
|
|
93
|
+
if (logger) {
|
|
94
|
+
logger.warning(msg);
|
|
95
|
+
} else {
|
|
96
|
+
console.warn(`⚠️ ${msg}`);
|
|
97
|
+
}
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Compare local and remote to determine sync status/conflict
|
|
102
|
+
const comparison = compareLocalAndRemote(filePath, { content, ...record });
|
|
103
|
+
|
|
104
|
+
// --- Conflict Handling ---
|
|
105
|
+
let finalContent = null; // Will hold the string to write if we should write the file
|
|
106
|
+
|
|
107
|
+
if (['missing', 'in_sync'].includes(comparison.status)) {
|
|
108
|
+
// No conflict, just write server version (create if missing)
|
|
109
|
+
finalContent = content;
|
|
110
|
+
} else {
|
|
111
|
+
// Attempt to read local file (may not exist)
|
|
112
|
+
let local = "";
|
|
113
|
+
|
|
114
|
+
const base = config.read(record.Id, { global: false, filename: 'base.json' });
|
|
115
|
+
const baseContent = decompressString(base?.compressedContent) || '';
|
|
116
|
+
|
|
117
|
+
// const base = config.read(filePath);
|
|
118
|
+
try { local = fs.readFileSync(filePath, "utf8"); } catch { }
|
|
119
|
+
|
|
120
|
+
if (resolutionMethod === 'skip') {
|
|
121
|
+
// Do nothing, never create or update file
|
|
122
|
+
console.log(chalk.yellow(`⏭️ Skipped ${record.Name} (${record.Type}) due to conflict or mismatch.`));
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
else if (resolutionMethod === 'overwrite') {
|
|
126
|
+
// Always accept server version, creating file if missing
|
|
127
|
+
console.log(chalk.redBright(`⚠️ Overwriting local file: ${filePath} with server version.`));
|
|
128
|
+
finalContent = content;
|
|
129
|
+
}
|
|
130
|
+
else if (resolutionMethod === 'diff') {
|
|
131
|
+
// Use mergeFiles, show diff, ask user
|
|
132
|
+
let merged;
|
|
133
|
+
try {
|
|
134
|
+
merged = mergeFiles(baseContent, local, content);
|
|
135
|
+
} catch (e) {
|
|
136
|
+
console.log(chalk.red(`❌ Error merging: ${e.message}`));
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Attempt to open the diff in vs code
|
|
141
|
+
const randomStr = (length) => Math.random().toString(36).slice(2, 2 + length);
|
|
142
|
+
const tempFileServer = path.join(tmpdir(), `${randomStr(4)}.Server${filename}`);
|
|
143
|
+
const tempFileLocal = path.join(tmpdir(), `${randomStr(4)}.Local${filename}`);
|
|
144
|
+
fs.writeFileSync(tempFileServer, content);
|
|
145
|
+
fs.writeFileSync(tempFileLocal, local);
|
|
146
|
+
|
|
147
|
+
const vsCodeDiffDisplayed = openDiffInVSCode(tempFileServer, tempFileLocal);
|
|
148
|
+
|
|
149
|
+
// If the VS Code diff failed, show in terminal
|
|
150
|
+
if (!vsCodeDiffDisplayed) {
|
|
151
|
+
// Show diff (local vs merged)
|
|
152
|
+
const diff = diffLines(local, merged);
|
|
153
|
+
console.log(chalk.magenta.bold(`\n======= REVIEW MERGED for ${filename} =======`));
|
|
154
|
+
diff.forEach(part => {
|
|
155
|
+
const color = part.added ? 'green' :
|
|
156
|
+
part.removed ? 'red' : 'gray';
|
|
157
|
+
process.stdout.write(chalk[color](part.value));
|
|
158
|
+
});
|
|
159
|
+
console.log(chalk.magenta.bold("\n======= END REVIEW ======="));
|
|
160
|
+
|
|
161
|
+
// Warn if there are still unresolved conflict markers
|
|
162
|
+
if (merged.includes('<<<<<<<')) {
|
|
163
|
+
console.log(chalk.red.bold("\n⚠️ Merge conflict markers present. Please resolve manually after accepting."));
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Prompt user to accept or skip writing merged content
|
|
168
|
+
const choice = readlineSync.question(
|
|
169
|
+
chalk.yellow("\nAccept merged changes and write to file? (y/n): ")
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
// Delete the temp files
|
|
173
|
+
fs.unlinkSync(tempFileLocal);
|
|
174
|
+
fs.unlinkSync(tempFileServer);
|
|
175
|
+
|
|
176
|
+
if (choice.trim().toLowerCase() === 'y') {
|
|
177
|
+
finalContent = merged;
|
|
178
|
+
} else {
|
|
179
|
+
console.log(chalk.yellow("⏭️ Skipped due to user choice."));
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
else if (resolutionMethod === 'merge') {
|
|
184
|
+
// No prompt: always write merged result (may contain conflict markers)
|
|
185
|
+
let merged;
|
|
186
|
+
|
|
187
|
+
try {
|
|
188
|
+
merged = mergeFiles(baseContent, local, content);
|
|
189
|
+
} catch (e) {
|
|
190
|
+
console.log(chalk.red(`❌ Error merging: ${e.message}`));
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
if (merged.includes('<<<<<<<')) {
|
|
194
|
+
console.log(chalk.red.bold(`⚠️ Merge conflict markers written to ${filename}. Manual review required.`));
|
|
195
|
+
} else {
|
|
196
|
+
console.log(chalk.green(`✔️ Merged ${filename} automatically.`));
|
|
197
|
+
}
|
|
198
|
+
finalContent = merged;
|
|
199
|
+
}
|
|
200
|
+
else {
|
|
201
|
+
// Unknown or unsupported mode (should not occur)
|
|
202
|
+
console.log(chalk.red(`❌ Unknown resolution method: ${resolutionMethod}`));
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// --- Actually Write File (if required by resolution) ---
|
|
208
|
+
if (finalContent !== null) {
|
|
209
|
+
try {
|
|
210
|
+
// Lookup a matching file dir in our cache, in case the file was renamed
|
|
211
|
+
const cachedFilePath = findFileByTag(record.Id) || path.resolve(filePath);
|
|
212
|
+
|
|
213
|
+
// Ensure the directory exists (paranoia for deeply nested files)
|
|
214
|
+
fs.mkdirSync(path.dirname(cachedFilePath), { recursive: true });
|
|
215
|
+
fs.writeFileSync(cachedFilePath, finalContent, "utf8");
|
|
216
|
+
|
|
217
|
+
// Add a file tag so we can keep track of file changes
|
|
218
|
+
await setFileTag(cachedFilePath, record.Id);
|
|
219
|
+
|
|
220
|
+
// Set mtime/atime for reproducible syncs (optional, best effort)
|
|
221
|
+
let mtimeMs = null;
|
|
222
|
+
if (record.ModifiedOn) {
|
|
223
|
+
mtimeMs = new Date(record.ModifiedOn).getTime();
|
|
224
|
+
} else if (record.CreatedOn) {
|
|
225
|
+
mtimeMs = new Date(record.CreatedOn).getTime();
|
|
226
|
+
}
|
|
227
|
+
if (mtimeMs) {
|
|
228
|
+
const mtimeSeconds = mtimeMs / 1000;
|
|
229
|
+
try {
|
|
230
|
+
fs.utimesSync(cachedFilePath, mtimeSeconds, mtimeSeconds);
|
|
231
|
+
} catch (e) {
|
|
232
|
+
const msg = `Could not set times for ${cachedFilePath}: ${e.message}`;
|
|
233
|
+
if (logger) {
|
|
234
|
+
logger.warning(msg);
|
|
235
|
+
} else {
|
|
236
|
+
console.warn(`⚠️ ${msg}`);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
// If the server records content and the local files content match then we can update the base
|
|
243
|
+
if (sha256(finalContent) === sha256(record.Content)) {
|
|
244
|
+
// We still need to use the expected filePath as the rename might be local only
|
|
245
|
+
updateBase(filePath, record, cachedFilePath);
|
|
246
|
+
}
|
|
247
|
+
} catch (err) {
|
|
248
|
+
const msg = `Failed to write file ${filePath}: ${err.message}`;
|
|
249
|
+
if (logger) {
|
|
250
|
+
logger.error(msg, err);
|
|
251
|
+
} else {
|
|
252
|
+
console.error(chalk.red(`❌ ${msg}`));
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Update progress (outside try-catch so it always updates)
|
|
258
|
+
if (progress && (i % 10 === 0 || i === records.length - 1)) {
|
|
259
|
+
progress.updateProgress('write', i + 1, records.length, `Writing files...`);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
};
|