@magentrix-corp/magentrix-cli 1.2.0 → 1.3.0
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 +282 -2
- package/actions/autopublish.js +9 -48
- package/actions/iris/buildStage.js +330 -0
- package/actions/iris/delete.js +211 -0
- package/actions/iris/dev.js +338 -0
- package/actions/iris/index.js +6 -0
- package/actions/iris/link.js +377 -0
- package/actions/iris/recover.js +228 -0
- package/actions/publish.js +258 -15
- package/actions/pull.js +520 -327
- package/actions/setup.js +62 -15
- package/bin/magentrix.js +43 -1
- package/package.json +2 -1
- package/utils/autopublishLock.js +77 -0
- package/utils/cli/helpers/compare.js +4 -5
- package/utils/cli/helpers/ensureApiKey.js +28 -22
- package/utils/cli/helpers/ensureInstanceUrl.js +35 -27
- package/utils/cli/writeRecords.js +13 -2
- package/utils/config.js +76 -0
- package/utils/iris/backup.js +201 -0
- package/utils/iris/builder.js +304 -0
- package/utils/iris/config-reader.js +296 -0
- package/utils/iris/deleteHelper.js +102 -0
- package/utils/iris/linker.js +490 -0
- package/utils/iris/validator.js +281 -0
- package/utils/iris/zipper.js +239 -0
- package/utils/logger.js +13 -5
- package/utils/magentrix/api/auth.js +45 -6
- package/utils/magentrix/api/iris.js +235 -0
- package/utils/permissionError.js +70 -0
- package/utils/progress.js +87 -1
- package/utils/updateFileBase.js +14 -2
- package/vars/global.js +1 -0
package/actions/pull.js
CHANGED
|
@@ -5,16 +5,18 @@ import fs from "fs";
|
|
|
5
5
|
import { withSpinner } from "../utils/spinner.js";
|
|
6
6
|
import { ProgressTracker } from "../utils/progress.js";
|
|
7
7
|
import { createLogger, Logger } from "../utils/logger.js";
|
|
8
|
-
import { EXPORT_ROOT, TYPE_DIR_MAP } from "../vars/global.js";
|
|
8
|
+
import { EXPORT_ROOT, TYPE_DIR_MAP, IRIS_APPS_DIR } from "../vars/global.js";
|
|
9
9
|
import { mapRecordToFile, writeRecords } from "../utils/cli/writeRecords.js";
|
|
10
|
-
import { updateBase, removeFromBase } from "../utils/updateFileBase.js";
|
|
10
|
+
import { updateBase, removeFromBase, removeFromBaseBulk } from "../utils/updateFileBase.js";
|
|
11
11
|
import { compareAllFilesAndLogStatus, promptConflictResolution, showCurrentConflicts } from "../utils/cli/helpers/compare.js";
|
|
12
12
|
import path from "path";
|
|
13
13
|
import { compareLocalAndRemote } from "../utils/compare.js";
|
|
14
14
|
import chalk from 'chalk';
|
|
15
|
-
import { getFileTag } from "../utils/filetag.js";
|
|
15
|
+
import { getFileTag, setFileTag } from "../utils/filetag.js";
|
|
16
16
|
import { downloadAssetsZip, listAssets } from "../utils/magentrix/api/assets.js";
|
|
17
17
|
import { downloadAssets, walkAssets } from "../utils/downloadAssets.js";
|
|
18
|
+
import { listApps, downloadApp } from "../utils/magentrix/api/iris.js";
|
|
19
|
+
import { extractIrisZip, hashIrisAppFolder } from "../utils/iris/zipper.js";
|
|
18
20
|
import { v4 as uuidv4 } from 'uuid';
|
|
19
21
|
import readlineSync from 'readline-sync';
|
|
20
22
|
|
|
@@ -55,6 +57,7 @@ export const pull = async () => {
|
|
|
55
57
|
progress.addStep('load', 'Loading cached data...');
|
|
56
58
|
progress.addStep('download-code', 'Downloading code entities...', { hasProgress: true });
|
|
57
59
|
progress.addStep('download-assets', 'Downloading static assets...', { hasProgress: true });
|
|
60
|
+
progress.addStep('download-iris', 'Downloading Iris apps...', { hasProgress: true });
|
|
58
61
|
progress.addStep('process', 'Processing files...', { hasProgress: true });
|
|
59
62
|
progress.addStep('conflicts', 'Checking for conflicts...');
|
|
60
63
|
progress.addStep('write', 'Writing files...', { hasProgress: true });
|
|
@@ -72,380 +75,567 @@ export const pull = async () => {
|
|
|
72
75
|
const instanceChanged = lastInstanceUrl && lastInstanceUrl !== instanceUrl;
|
|
73
76
|
|
|
74
77
|
if (instanceChanged) {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
78
|
+
progress.stopSpinner();
|
|
79
|
+
progress.render();
|
|
80
|
+
console.log(chalk.yellow.bold(`\n⚠️ INSTANCE CHANGE DETECTED`));
|
|
81
|
+
console.log(chalk.yellow(`Previous instance: ${chalk.cyan(lastInstanceUrl)}`));
|
|
82
|
+
console.log(chalk.yellow(`New instance: ${chalk.cyan(instanceUrl)}`));
|
|
83
|
+
console.log();
|
|
84
|
+
console.log(chalk.red.bold(`⚠️ WARNING: This will DELETE your existing ${chalk.white(EXPORT_ROOT + '/')} directory!`));
|
|
85
|
+
console.log(chalk.gray(`This is necessary to prevent mixing files from different instances.`));
|
|
86
|
+
console.log();
|
|
87
|
+
|
|
88
|
+
const confirm = readlineSync.question(
|
|
89
|
+
chalk.yellow(`Type ${chalk.white.bold('yes')} to continue and delete ${EXPORT_ROOT}/, or ${chalk.white.bold('no')} to cancel: `)
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
if (confirm.trim().toLowerCase() !== 'yes') {
|
|
93
|
+
console.log(chalk.red('\n❌ Pull cancelled. No files were deleted.'));
|
|
94
|
+
console.log(chalk.gray(`Tip: To pull from ${chalk.cyan(lastInstanceUrl)}, switch back to those credentials.`));
|
|
95
|
+
process.exit(0);
|
|
96
|
+
}
|
|
88
97
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
98
|
+
console.log(chalk.yellow(`\n🗑️ Removing existing ${EXPORT_ROOT}/ directory...`));
|
|
99
|
+
if (fs.existsSync(EXPORT_ROOT)) {
|
|
100
|
+
fs.rmSync(EXPORT_ROOT, { recursive: true, force: true });
|
|
101
|
+
}
|
|
102
|
+
// Clear the base.json cache as well since it's from a different instance
|
|
103
|
+
fs.writeFileSync('.magentrix/base.json', JSON.stringify({}));
|
|
104
|
+
config.save('cachedFiles', {}, { filename: 'fileCache.json' });
|
|
105
|
+
config.save('trackedFileTags', {}, { filename: 'fileIdIndex.json' });
|
|
106
|
+
console.log(chalk.green(`✓ Removed ${EXPORT_ROOT}/ directory\n`));
|
|
107
|
+
|
|
108
|
+
progress.start(); // Restart progress tracker
|
|
109
|
+
progress.startStep('check');
|
|
93
110
|
}
|
|
94
111
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
112
|
+
// Save the current instance URL for future comparisons
|
|
113
|
+
config.save('lastInstanceUrl', instanceUrl, { global: false, filename: 'config.json' });
|
|
114
|
+
progress.completeStep('check', instanceChanged ? '✓ Instance reset' : '✓ Instance verified');
|
|
115
|
+
|
|
116
|
+
// Step 3: Prepare queries for both ActiveClass and ActivePage
|
|
117
|
+
const queries = [
|
|
118
|
+
{
|
|
119
|
+
name: "ActiveClass",
|
|
120
|
+
query: "SELECT Id,Body,Name,CreatedOn,Description,ModifiedOn,Type FROM ActiveClass",
|
|
121
|
+
contentField: "Body",
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
name: "ActivePage",
|
|
125
|
+
query: "SELECT Id,Content,Name,CreatedOn,Description,ModifiedOn,Type FROM ActivePage",
|
|
126
|
+
contentField: "Content",
|
|
127
|
+
}
|
|
128
|
+
];
|
|
129
|
+
|
|
130
|
+
// Step 3: Load existing base.json to detect deletions
|
|
131
|
+
progress.startStep('load');
|
|
132
|
+
const hits = await config.searchObject({}, { filename: "base.json", global: false });
|
|
133
|
+
const cachedResults = hits?.[0]?.value || {};
|
|
134
|
+
const cachedAssets = Object.values(cachedResults).filter(c =>
|
|
135
|
+
c.type === 'File' || c.type === 'Folder'
|
|
136
|
+
);
|
|
137
|
+
progress.completeStep('load', `✓ Loaded ${Object.keys(cachedResults).length} cached entries`);
|
|
138
|
+
|
|
139
|
+
// Step 4a: Download code entities (ActiveClass and ActivePage)
|
|
140
|
+
progress.startStep('download-code');
|
|
141
|
+
|
|
142
|
+
let activeClassResult, activePageResult;
|
|
143
|
+
const codeDownloadErrors = [];
|
|
144
|
+
|
|
145
|
+
try {
|
|
146
|
+
logger.info('Starting code entity downloads');
|
|
147
|
+
const meqlResults = await Promise.all(
|
|
148
|
+
queries.map(q => meqlQuery(instanceUrl, token.value, q.query))
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
[activeClassResult, activePageResult] = meqlResults;
|
|
152
|
+
const totalCodeEntities = activeClassResult.Records.length + activePageResult.Records.length;
|
|
153
|
+
|
|
154
|
+
logger.info(`Downloaded ${totalCodeEntities} code entities`, {
|
|
155
|
+
activeClass: activeClassResult.Records.length,
|
|
156
|
+
activePage: activePageResult.Records.length
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
progress.updateProgress('download-code', totalCodeEntities, totalCodeEntities, `Downloaded ${totalCodeEntities} code entities`);
|
|
160
|
+
progress.completeStep('download-code', `✓ Downloaded ${totalCodeEntities} code entities`);
|
|
161
|
+
} catch (error) {
|
|
162
|
+
logger.error('Error downloading code entities', error);
|
|
163
|
+
codeDownloadErrors.push({ stage: 'code-download', error: error.message });
|
|
164
|
+
progress.failStep('download-code', error.message);
|
|
165
|
+
|
|
166
|
+
// Show error preview
|
|
167
|
+
progress.stopSpinner();
|
|
168
|
+
console.log('');
|
|
169
|
+
console.log(chalk.red.bold('❌ Code Download Error:'));
|
|
170
|
+
console.log(chalk.red(` ${error.message.substring(0, 200)}`));
|
|
171
|
+
console.log(chalk.cyan(`\n 📄 Full details in log file`));
|
|
172
|
+
console.log('');
|
|
173
|
+
|
|
174
|
+
// Initialize empty data structures
|
|
175
|
+
if (!activeClassResult) activeClassResult = { Records: [] };
|
|
176
|
+
if (!activePageResult) activePageResult = { Records: [] };
|
|
177
|
+
|
|
178
|
+
// Restart progress for next step
|
|
179
|
+
progress.start();
|
|
180
|
+
progress.startStep('download-assets');
|
|
98
181
|
}
|
|
99
|
-
// Clear the base.json cache as well since it's from a different instance
|
|
100
|
-
fs.writeFileSync('.magentrix/base.json', JSON.stringify({}));
|
|
101
|
-
config.save('cachedFiles', {}, { filename: 'fileCache.json' });
|
|
102
|
-
config.save('trackedFileTags', {}, { filename: 'fileIdIndex.json' });
|
|
103
|
-
console.log(chalk.green(`✓ Removed ${EXPORT_ROOT}/ directory\n`));
|
|
104
182
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
// Save the current instance URL for future comparisons
|
|
110
|
-
config.save('lastInstanceUrl', instanceUrl, { global: false, filename: 'config.json' });
|
|
111
|
-
progress.completeStep('check', instanceChanged ? '✓ Instance reset' : '✓ Instance verified');
|
|
112
|
-
|
|
113
|
-
// Step 3: Prepare queries for both ActiveClass and ActivePage
|
|
114
|
-
const queries = [
|
|
115
|
-
{
|
|
116
|
-
name: "ActiveClass",
|
|
117
|
-
query: "SELECT Id,Body,Name,CreatedOn,Description,ModifiedOn,Type FROM ActiveClass",
|
|
118
|
-
contentField: "Body",
|
|
119
|
-
},
|
|
120
|
-
{
|
|
121
|
-
name: "ActivePage",
|
|
122
|
-
query: "SELECT Id,Content,Name,CreatedOn,Description,ModifiedOn,Type FROM ActivePage",
|
|
123
|
-
contentField: "Content",
|
|
183
|
+
// Step 4b: Download static assets
|
|
184
|
+
if (!codeDownloadErrors.length) {
|
|
185
|
+
progress.startStep('download-assets');
|
|
124
186
|
}
|
|
125
|
-
];
|
|
126
|
-
|
|
127
|
-
// Step 3: Load existing base.json to detect deletions
|
|
128
|
-
progress.startStep('load');
|
|
129
|
-
const hits = await config.searchObject({}, { filename: "base.json", global: false });
|
|
130
|
-
const cachedResults = hits?.[0]?.value || {};
|
|
131
|
-
const cachedAssets = Object.values(cachedResults).filter(c =>
|
|
132
|
-
c.type === 'File' || c.type === 'Folder'
|
|
133
|
-
);
|
|
134
|
-
progress.completeStep('load', `✓ Loaded ${Object.keys(cachedResults).length} cached entries`);
|
|
135
|
-
|
|
136
|
-
// Step 4a: Download code entities (ActiveClass and ActivePage)
|
|
137
|
-
progress.startStep('download-code');
|
|
138
|
-
|
|
139
|
-
let activeClassResult, activePageResult;
|
|
140
|
-
const codeDownloadErrors = [];
|
|
141
|
-
|
|
142
|
-
try {
|
|
143
|
-
logger.info('Starting code entity downloads');
|
|
144
|
-
const meqlResults = await Promise.all(
|
|
145
|
-
queries.map(q => meqlQuery(instanceUrl, token.value, q.query))
|
|
146
|
-
);
|
|
147
|
-
|
|
148
|
-
[activeClassResult, activePageResult] = meqlResults;
|
|
149
|
-
const totalCodeEntities = activeClassResult.Records.length + activePageResult.Records.length;
|
|
150
|
-
|
|
151
|
-
logger.info(`Downloaded ${totalCodeEntities} code entities`, {
|
|
152
|
-
activeClass: activeClassResult.Records.length,
|
|
153
|
-
activePage: activePageResult.Records.length
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
progress.updateProgress('download-code', totalCodeEntities, totalCodeEntities, `Downloaded ${totalCodeEntities} code entities`);
|
|
157
|
-
progress.completeStep('download-code', `✓ Downloaded ${totalCodeEntities} code entities`);
|
|
158
|
-
} catch (error) {
|
|
159
|
-
logger.error('Error downloading code entities', error);
|
|
160
|
-
codeDownloadErrors.push({ stage: 'code-download', error: error.message });
|
|
161
|
-
progress.failStep('download-code', error.message);
|
|
162
187
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
188
|
+
let assets;
|
|
189
|
+
const assetDownloadErrors = [];
|
|
190
|
+
|
|
191
|
+
try {
|
|
192
|
+
logger.info('Starting static asset downloads');
|
|
193
|
+
assets = await downloadAssets(instanceUrl, token.value, null, (current, total, message) => {
|
|
194
|
+
progress.updateProgress('download-assets', current, total, message);
|
|
195
|
+
}, logger);
|
|
196
|
+
|
|
197
|
+
logger.info(`Downloaded ${assets.tree.length} asset folders`);
|
|
198
|
+
progress.completeStep('download-assets', `✓ Downloaded ${assets.tree.length} asset folders`);
|
|
199
|
+
} catch (error) {
|
|
200
|
+
logger.error('Error downloading static assets', error);
|
|
201
|
+
assetDownloadErrors.push({ stage: 'asset-download', error: error.message });
|
|
202
|
+
progress.failStep('download-assets', error.message);
|
|
203
|
+
|
|
204
|
+
// Show error preview
|
|
205
|
+
progress.stopSpinner();
|
|
206
|
+
console.log('');
|
|
207
|
+
console.log(chalk.red.bold('❌ Asset Download Error:'));
|
|
208
|
+
console.log(chalk.red(` ${error.message.substring(0, 200)}`));
|
|
209
|
+
console.log(chalk.cyan(`\n 📄 Full details in log file`));
|
|
210
|
+
console.log('');
|
|
211
|
+
|
|
212
|
+
// Initialize empty data structure
|
|
213
|
+
if (!assets) assets = { tree: [] };
|
|
214
|
+
|
|
215
|
+
// Restart progress for next step
|
|
216
|
+
progress.start();
|
|
217
|
+
}
|
|
179
218
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
219
|
+
// Step 4c: Download Iris apps
|
|
220
|
+
progress.startStep('download-iris');
|
|
221
|
+
let irisApps = [];
|
|
222
|
+
const irisDownloadErrors = [];
|
|
184
223
|
|
|
185
|
-
|
|
186
|
-
|
|
224
|
+
try {
|
|
225
|
+
logger.info('Fetching Iris apps list');
|
|
226
|
+
const irisListResult = await listApps(instanceUrl, token.value);
|
|
187
227
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
progress.updateProgress('download-assets', current, total, message);
|
|
192
|
-
}, logger);
|
|
228
|
+
if (irisListResult.success && irisListResult.apps && irisListResult.apps.length > 0) {
|
|
229
|
+
irisApps = irisListResult.apps;
|
|
230
|
+
const irisAppsDir = path.resolve(EXPORT_ROOT, IRIS_APPS_DIR);
|
|
193
231
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
assetDownloadErrors.push({ stage: 'asset-download', error: error.message });
|
|
199
|
-
progress.failStep('download-assets', error.message);
|
|
232
|
+
// Ensure iris-apps directory exists
|
|
233
|
+
if (!fs.existsSync(irisAppsDir)) {
|
|
234
|
+
fs.mkdirSync(irisAppsDir, { recursive: true });
|
|
235
|
+
}
|
|
200
236
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
237
|
+
logger.info(`Found ${irisApps.length} Iris apps to download`);
|
|
238
|
+
|
|
239
|
+
for (let i = 0; i < irisApps.length; i++) {
|
|
240
|
+
const app = irisApps[i];
|
|
241
|
+
progress.updateProgress('download-iris', i + 1, irisApps.length, `Downloading ${app.folderName}...`);
|
|
242
|
+
|
|
243
|
+
try {
|
|
244
|
+
// Download the app as a buffer
|
|
245
|
+
const { buffer } = await downloadApp(instanceUrl, token.value, app.folderName);
|
|
246
|
+
|
|
247
|
+
// Extract to iris-apps directory
|
|
248
|
+
const appDir = path.join(irisAppsDir, app.folderName);
|
|
249
|
+
|
|
250
|
+
// Remove existing app directory if it exists
|
|
251
|
+
if (fs.existsSync(appDir)) {
|
|
252
|
+
fs.rmSync(appDir, { recursive: true, force: true });
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Extract the zip (it contains a folder with the app name)
|
|
256
|
+
await extractIrisZip(buffer, irisAppsDir);
|
|
257
|
+
|
|
258
|
+
// Calculate content hash for change detection
|
|
259
|
+
const contentHash = hashIrisAppFolder(appDir);
|
|
260
|
+
|
|
261
|
+
// Update base.json for this Iris app
|
|
262
|
+
updateBase(appDir, {
|
|
263
|
+
Id: `iris-app:${app.folderName}`,
|
|
264
|
+
Type: 'IrisApp',
|
|
265
|
+
folderName: app.folderName,
|
|
266
|
+
uploadedOn: app.uploadedOn,
|
|
267
|
+
modifiedOn: app.modifiedOn,
|
|
268
|
+
size: app.size,
|
|
269
|
+
contentHash // Store hash for change detection
|
|
270
|
+
}, appDir);
|
|
271
|
+
|
|
272
|
+
logger.info(`Downloaded Iris app: ${app.folderName}`);
|
|
273
|
+
} catch (appError) {
|
|
274
|
+
// Detect permission errors and provide helpful hint
|
|
275
|
+
const isPermissionError = appError.code === 'EACCES' || appError.code === 'EPERM' ||
|
|
276
|
+
appError.message?.includes('permission denied') || appError.message?.includes('EACCES');
|
|
277
|
+
|
|
278
|
+
// Create a more informative error message
|
|
279
|
+
const errorDetail = appError.message || String(appError);
|
|
280
|
+
const errorMsg = `Failed to download Iris app ${app.folderName}: ${errorDetail}`;
|
|
281
|
+
|
|
282
|
+
if (isPermissionError) {
|
|
283
|
+
const hint = `Try: sudo chown -R $(whoami):staff "${irisAppsDir}"`;
|
|
284
|
+
logger.error(errorMsg, appError, null, hint);
|
|
285
|
+
irisDownloadErrors.push({ app: app.folderName, error: appError.message, isPermissionError: true, hint });
|
|
286
|
+
progress.addIssue('error', errorMsg, hint);
|
|
287
|
+
} else {
|
|
288
|
+
logger.error(errorMsg, appError);
|
|
289
|
+
irisDownloadErrors.push({ app: app.folderName, error: appError.message });
|
|
290
|
+
progress.addIssue('error', errorMsg);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
208
294
|
|
|
209
|
-
|
|
210
|
-
|
|
295
|
+
progress.completeStep('download-iris', `✓ Downloaded ${irisApps.length} Iris apps`);
|
|
296
|
+
} else {
|
|
297
|
+
progress.completeStep('download-iris', '✓ No Iris apps found');
|
|
298
|
+
}
|
|
299
|
+
} catch (error) {
|
|
300
|
+
logger.error('Error fetching Iris apps', error);
|
|
301
|
+
irisDownloadErrors.push({ stage: 'iris-list', error: error.message });
|
|
302
|
+
progress.completeStep('download-iris', '⚠ Iris apps skipped (API may not be available)');
|
|
303
|
+
}
|
|
211
304
|
|
|
212
|
-
//
|
|
213
|
-
|
|
214
|
-
|
|
305
|
+
// Check if we have any data to continue with
|
|
306
|
+
if (!activeClassResult?.Records?.length && !activePageResult?.Records?.length && !assets?.tree?.length) {
|
|
307
|
+
throw new Error('Download failed completely. No data available to continue.');
|
|
308
|
+
}
|
|
215
309
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
310
|
+
// Step 5: Process files and detect changes
|
|
311
|
+
progress.startStep('process');
|
|
312
|
+
const { allRecords, issues, deletionLogs, processingErrors } = await (async () => {
|
|
313
|
+
let processedCount = 0;
|
|
314
|
+
const totalToProcess = cachedAssets.length + (activeClassResult.Records.length + activePageResult.Records.length);
|
|
315
|
+
const processingErrors = [];
|
|
316
|
+
|
|
317
|
+
// Collect all server asset paths
|
|
318
|
+
const serverAssetPaths = new Set();
|
|
319
|
+
const collectServerPaths = (records) => {
|
|
320
|
+
try {
|
|
321
|
+
for (const record of records) {
|
|
322
|
+
const fullPath = path.join(EXPORT_ROOT, record?.Path);
|
|
323
|
+
serverAssetPaths.add(path.normalize(fullPath));
|
|
324
|
+
|
|
325
|
+
if (record?.Type === 'Folder' && record?.Children?.length > 0) {
|
|
326
|
+
collectServerPaths(record.Children);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
} catch (err) {
|
|
330
|
+
processingErrors.push({ stage: 'collect-paths', error: err.message });
|
|
331
|
+
logger.error('Error collecting server paths', err);
|
|
332
|
+
}
|
|
333
|
+
};
|
|
334
|
+
collectServerPaths(assets.tree);
|
|
335
|
+
progress.updateProgress('process', ++processedCount, totalToProcess, 'Collecting server paths');
|
|
336
|
+
|
|
337
|
+
// Detect and delete assets that were on server before but are now gone
|
|
338
|
+
const assetsToDelete = [];
|
|
339
|
+
for (const cached of cachedAssets) {
|
|
340
|
+
const cachedPath = path.normalize(cached.lastKnownActualPath || cached.filePath || cached.lastKnownPath);
|
|
341
|
+
|
|
342
|
+
// If this asset was in base.json but not returned from server, it was deleted
|
|
343
|
+
if (!serverAssetPaths.has(cachedPath)) {
|
|
344
|
+
assetsToDelete.push(cachedPath);
|
|
345
|
+
}
|
|
220
346
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
347
|
+
processedCount++;
|
|
348
|
+
if (processedCount % 50 === 0) {
|
|
349
|
+
progress.updateProgress('process', processedCount, totalToProcess, `Checking assets...`);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
progress.updateProgress('process', processedCount, totalToProcess, `Found ${assetsToDelete.length} assets to delete`);
|
|
353
|
+
|
|
354
|
+
// Count total assets (for progress tracking)
|
|
355
|
+
// Note: We don't update base.json for assets here because:
|
|
356
|
+
// 1. Assets are already downloaded and written to disk
|
|
357
|
+
// 2. Their base will be updated during the "Writing files" stage if modified
|
|
358
|
+
// 3. Updating base.json thousands of times here is extremely slow
|
|
359
|
+
logger.info('Counting assets in tree');
|
|
360
|
+
let assetCount = 0;
|
|
361
|
+
const countAssets = (records) => {
|
|
232
362
|
for (const record of records) {
|
|
233
|
-
|
|
234
|
-
serverAssetPaths.add(path.normalize(fullPath));
|
|
235
|
-
|
|
363
|
+
assetCount++;
|
|
236
364
|
if (record?.Type === 'Folder' && record?.Children?.length > 0) {
|
|
237
|
-
|
|
365
|
+
countAssets(record.Children);
|
|
238
366
|
}
|
|
239
367
|
}
|
|
240
|
-
} catch (err) {
|
|
241
|
-
processingErrors.push({ stage: 'collect-paths', error: err.message });
|
|
242
|
-
logger.error('Error collecting server paths', err);
|
|
243
|
-
}
|
|
244
|
-
};
|
|
245
|
-
collectServerPaths(assets.tree);
|
|
246
|
-
progress.updateProgress('process', ++processedCount, totalToProcess, 'Collecting server paths');
|
|
247
|
-
|
|
248
|
-
// Detect and delete assets that were on server before but are now gone
|
|
249
|
-
const assetsToDelete = [];
|
|
250
|
-
for (const cached of cachedAssets) {
|
|
251
|
-
const cachedPath = path.normalize(cached.lastKnownActualPath || cached.filePath || cached.lastKnownPath);
|
|
252
|
-
|
|
253
|
-
// If this asset was in base.json but not returned from server, it was deleted
|
|
254
|
-
if (!serverAssetPaths.has(cachedPath)) {
|
|
255
|
-
assetsToDelete.push(cachedPath);
|
|
256
368
|
}
|
|
257
369
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
370
|
+
countAssets(assets.tree);
|
|
371
|
+
logger.info(`Counted ${assetCount} total assets`);
|
|
372
|
+
processedCount += assetCount;
|
|
373
|
+
progress.updateProgress('process', processedCount, totalToProcess, `Processed ${assetCount} assets`);
|
|
374
|
+
|
|
375
|
+
// Handle code entity (ActiveClass, ActivePage) deletions
|
|
376
|
+
const activeClassRecords = (activeClassResult.Records || []).map(record => {
|
|
377
|
+
record.Content = record.Body;
|
|
378
|
+
delete record.Body;
|
|
379
|
+
return record;
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
const activePageRecords = (activePageResult.Records || []);
|
|
383
|
+
const allRecords = [...activeClassRecords, ...activePageRecords].map(mapRecordToFile);
|
|
384
|
+
|
|
385
|
+
// Get all server record IDs
|
|
386
|
+
const serverRecordIds = new Set([
|
|
387
|
+
...activeClassRecords.map(r => r.Id),
|
|
388
|
+
...activePageRecords.map(r => r.Id)
|
|
389
|
+
]);
|
|
390
|
+
|
|
391
|
+
// Find code entities in base.json that are no longer on server
|
|
392
|
+
// Exclude Files, Folders, and IrisApps - only ActiveClass and ActivePage
|
|
393
|
+
const cachedCodeEntities = Object.values(cachedResults).filter(c =>
|
|
394
|
+
c.type !== 'File' && c.type !== 'Folder' && c.type !== 'IrisApp' && c.recordId
|
|
395
|
+
);
|
|
396
|
+
|
|
397
|
+
const codeEntitiesToDelete = [];
|
|
398
|
+
for (const cached of cachedCodeEntities) {
|
|
399
|
+
// If this code entity was in base.json but not returned from server, it was deleted
|
|
400
|
+
if (!serverRecordIds.has(cached.recordId)) {
|
|
401
|
+
codeEntitiesToDelete.push(cached);
|
|
402
|
+
}
|
|
261
403
|
}
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
404
|
+
|
|
405
|
+
// Delete local files/folders that were deleted on server
|
|
406
|
+
logger.info(`Starting deletion of ${assetsToDelete.length} assets`);
|
|
407
|
+
const deletionLogs = [];
|
|
408
|
+
for (const assetPath of assetsToDelete) {
|
|
409
|
+
try {
|
|
410
|
+
if (fs.existsSync(assetPath)) {
|
|
411
|
+
const stats = fs.statSync(assetPath);
|
|
412
|
+
if (stats.isDirectory()) {
|
|
413
|
+
fs.rmSync(assetPath, { recursive: true, force: true });
|
|
414
|
+
deletionLogs.push({ type: 'folder', path: assetPath });
|
|
415
|
+
logger.info('Deleted folder', { path: assetPath });
|
|
416
|
+
} else {
|
|
417
|
+
fs.unlinkSync(assetPath);
|
|
418
|
+
deletionLogs.push({ type: 'file', path: assetPath });
|
|
419
|
+
logger.info('Deleted file', { path: assetPath });
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
} catch (err) {
|
|
423
|
+
deletionLogs.push({ type: 'error', path: assetPath, error: err.message });
|
|
424
|
+
logger.error(`Failed to delete asset: ${assetPath}`, err);
|
|
277
425
|
}
|
|
278
426
|
}
|
|
279
|
-
}
|
|
280
427
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
// Handle code entity (ActiveClass, ActivePage) deletions
|
|
287
|
-
const activeClassRecords = (activeClassResult.Records || []).map(record => {
|
|
288
|
-
record.Content = record.Body;
|
|
289
|
-
delete record.Body;
|
|
290
|
-
return record;
|
|
291
|
-
});
|
|
292
|
-
|
|
293
|
-
const activePageRecords = (activePageResult.Records || []);
|
|
294
|
-
const allRecords = [...activeClassRecords, ...activePageRecords].map(mapRecordToFile);
|
|
295
|
-
|
|
296
|
-
// Get all server record IDs
|
|
297
|
-
const serverRecordIds = new Set([
|
|
298
|
-
...activeClassRecords.map(r => r.Id),
|
|
299
|
-
...activePageRecords.map(r => r.Id)
|
|
300
|
-
]);
|
|
301
|
-
|
|
302
|
-
// Find code entities in base.json that are no longer on server
|
|
303
|
-
const cachedCodeEntities = Object.values(cachedResults).filter(c =>
|
|
304
|
-
c.type !== 'File' && c.type !== 'Folder' && c.recordId
|
|
305
|
-
);
|
|
428
|
+
// Bulk remove from base.json
|
|
429
|
+
const assetPathsToRemove = deletionLogs
|
|
430
|
+
.filter(l => l.type === 'file' || l.type === 'folder')
|
|
431
|
+
.map(l => l.path);
|
|
306
432
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
// If this code entity was in base.json but not returned from server, it was deleted
|
|
310
|
-
if (!serverRecordIds.has(cached.recordId)) {
|
|
311
|
-
codeEntitiesToDelete.push(cached);
|
|
433
|
+
if (assetPathsToRemove.length > 0) {
|
|
434
|
+
removeFromBaseBulk(assetPathsToRemove);
|
|
312
435
|
}
|
|
313
|
-
}
|
|
314
436
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
437
|
+
// Delete local code entity files that were deleted on server
|
|
438
|
+
logger.info(`Starting deletion of ${codeEntitiesToDelete.length} code entities`);
|
|
439
|
+
for (const entity of codeEntitiesToDelete) {
|
|
440
|
+
const entityPath = entity.filePath || entity.lastKnownPath;
|
|
441
|
+
try {
|
|
442
|
+
if (entityPath && fs.existsSync(entityPath)) {
|
|
443
|
+
fs.unlinkSync(entityPath);
|
|
444
|
+
deletionLogs.push({ type: 'entity', path: entityPath, entityType: entity.type });
|
|
445
|
+
logger.info('Deleted code entity', { path: entityPath, type: entity.type });
|
|
446
|
+
}
|
|
447
|
+
} catch (err) {
|
|
448
|
+
// Detect permission errors
|
|
449
|
+
const isPermissionError = err.code === 'EACCES' || err.code === 'EPERM';
|
|
450
|
+
deletionLogs.push({ type: 'error', path: entityPath, error: err.message, isPermissionError });
|
|
451
|
+
|
|
452
|
+
if (isPermissionError) {
|
|
453
|
+
const hint = `Try: sudo chown -R $(whoami):staff "${path.dirname(entityPath)}"`;
|
|
454
|
+
logger.error(`Failed to delete code entity: ${entityPath}`, err, null, hint);
|
|
455
|
+
progress.addIssue('error', `Failed to delete code entity: ${entityPath}`, hint);
|
|
326
456
|
} else {
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
logger.info('Deleted file', { path: assetPath });
|
|
457
|
+
logger.error(`Failed to delete code entity: ${entityPath}`, err);
|
|
458
|
+
progress.addIssue('error', `Failed to delete code entity: ${entityPath}`);
|
|
330
459
|
}
|
|
331
460
|
}
|
|
332
|
-
// Remove from base.json
|
|
333
|
-
removeFromBase(assetPath);
|
|
334
|
-
} catch (err) {
|
|
335
|
-
deletionLogs.push({ type: 'error', path: assetPath, error: err.message });
|
|
336
|
-
logger.error(`Failed to delete asset: ${assetPath}`, err);
|
|
337
461
|
}
|
|
338
|
-
}
|
|
339
462
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
logger.error(`Failed to delete code entity: ${entityPath}`, err);
|
|
463
|
+
// Bulk remove code entities from base.json
|
|
464
|
+
const entityIdsToRemove = deletionLogs
|
|
465
|
+
.filter(l => l.type === 'entity')
|
|
466
|
+
.map(l => {
|
|
467
|
+
// Find the entity object that corresponds to this path to get the ID
|
|
468
|
+
const entity = codeEntitiesToDelete.find(e =>
|
|
469
|
+
(e.filePath === l.path || e.lastKnownPath === l.path)
|
|
470
|
+
);
|
|
471
|
+
return entity ? entity.recordId : null;
|
|
472
|
+
})
|
|
473
|
+
.filter(id => id !== null);
|
|
474
|
+
|
|
475
|
+
if (entityIdsToRemove.length > 0) {
|
|
476
|
+
removeFromBaseBulk(entityIdsToRemove);
|
|
355
477
|
}
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
logger.warning('Record has error', { record: record.Name || record.relativePath });
|
|
368
|
-
processingErrors.push({
|
|
369
|
-
stage: 'record-error',
|
|
370
|
-
file: record.Name || record.relativePath,
|
|
371
|
-
error: 'Record contains error flag'
|
|
372
|
-
});
|
|
373
|
-
continue;
|
|
478
|
+
logger.info(`Completed deletions - ${deletionLogs.length} items deleted`);
|
|
479
|
+
progress.updateProgress('process', processedCount, totalToProcess, `Deleted ${deletionLogs.length} items`);
|
|
480
|
+
|
|
481
|
+
// Compare files and detect conflicts
|
|
482
|
+
logger.info(`Starting file comparison for ${allRecords.length} records`);
|
|
483
|
+
// --- Fix for Bug 1: Rename Tracking ---
|
|
484
|
+
// Build a map of RecordId -> LocalPath from cached results
|
|
485
|
+
const localPathMap = new Map();
|
|
486
|
+
Object.values(cachedResults).forEach(entry => {
|
|
487
|
+
if (entry.recordId && (entry.filePath || entry.lastKnownPath)) {
|
|
488
|
+
localPathMap.set(entry.recordId, entry.filePath || entry.lastKnownPath);
|
|
374
489
|
}
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
// Check for renames before comparison
|
|
493
|
+
for (const record of allRecords) {
|
|
494
|
+
if (localPathMap.has(record.Id)) {
|
|
495
|
+
const oldPath = localPathMap.get(record.Id);
|
|
496
|
+
const newPath = path.join(EXPORT_ROOT, record.relativePath);
|
|
497
|
+
|
|
498
|
+
// Normalize paths for comparison
|
|
499
|
+
let normalizedOld = path.normalize(path.resolve(oldPath));
|
|
500
|
+
const normalizedNew = path.normalize(path.resolve(newPath));
|
|
501
|
+
|
|
502
|
+
// Fix for path resolution: base.json might store paths relative to CWD or EXPORT_ROOT
|
|
503
|
+
// If direct resolution fails, try prepending EXPORT_ROOT
|
|
504
|
+
if (!fs.existsSync(normalizedOld)) {
|
|
505
|
+
const withRoot = path.normalize(path.resolve(EXPORT_ROOT, oldPath));
|
|
506
|
+
if (fs.existsSync(withRoot)) {
|
|
507
|
+
normalizedOld = withRoot;
|
|
508
|
+
}
|
|
509
|
+
}
|
|
375
510
|
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
511
|
+
if (normalizedOld !== normalizedNew && fs.existsSync(normalizedOld)) {
|
|
512
|
+
try {
|
|
513
|
+
logger.info(`Detected rename: ${path.relative(EXPORT_ROOT, oldPath)} -> ${record.relativePath}`);
|
|
514
|
+
|
|
515
|
+
// Ensure target directory exists
|
|
516
|
+
fs.mkdirSync(path.dirname(normalizedNew), { recursive: true });
|
|
517
|
+
|
|
518
|
+
// Rename file
|
|
519
|
+
fs.renameSync(normalizedOld, normalizedNew);
|
|
520
|
+
|
|
521
|
+
// Update fileIdIndex.json to point to the new path
|
|
522
|
+
// This must be done BEFORE writeRecords runs, otherwise findFileByTag
|
|
523
|
+
// will return the old path and writeRecords will recreate the old file
|
|
524
|
+
await setFileTag(normalizedNew, record.Id);
|
|
525
|
+
|
|
526
|
+
// Update base.json immediately to reflect new path
|
|
527
|
+
// We use the existing record data but update the path
|
|
528
|
+
updateBase(record.relativePath, record, normalizedNew);
|
|
529
|
+
|
|
530
|
+
// Remove old path from deletion logs if it was marked for deletion
|
|
531
|
+
// (It might have been marked if the old filename wasn't in the server response)
|
|
532
|
+
const deletionIndex = deletionLogs.findIndex(l => path.normalize(l.path) === normalizedOld);
|
|
533
|
+
if (deletionIndex !== -1) {
|
|
534
|
+
deletionLogs.splice(deletionIndex, 1);
|
|
535
|
+
logger.info(`Cancelled deletion of renamed file: ${path.relative(EXPORT_ROOT, oldPath)}`);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
console.log(chalk.cyan(` ↻ Renamed: ${path.relative(EXPORT_ROOT, oldPath)} -> ${record.relativePath}`));
|
|
539
|
+
} catch (err) {
|
|
540
|
+
logger.error(`Failed to rename file: ${oldPath} -> ${newPath}`, err);
|
|
541
|
+
processingErrors.push({
|
|
542
|
+
stage: 'rename',
|
|
543
|
+
file: record.relativePath,
|
|
544
|
+
error: `Rename failed: ${err.message}`
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
}
|
|
379
548
|
}
|
|
549
|
+
}
|
|
550
|
+
// --- End Fix for Bug 1 ---
|
|
551
|
+
|
|
552
|
+
const issues = [];
|
|
553
|
+
for (let i = 0; i < allRecords.length; i++) {
|
|
554
|
+
const record = allRecords[i];
|
|
555
|
+
try {
|
|
556
|
+
if (record?.error) {
|
|
557
|
+
logger.warning('Record has error', { record: record.Name || record.relativePath });
|
|
558
|
+
processingErrors.push({
|
|
559
|
+
stage: 'record-error',
|
|
560
|
+
file: record.Name || record.relativePath,
|
|
561
|
+
error: 'Record contains error flag'
|
|
562
|
+
});
|
|
563
|
+
continue;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// Log every 50th file to track progress
|
|
567
|
+
if (i % 50 === 0) {
|
|
568
|
+
logger.info(`Comparing file ${i + 1}/${allRecords.length}: ${record.relativePath}`);
|
|
569
|
+
}
|
|
380
570
|
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
571
|
+
const status = compareLocalAndRemote(
|
|
572
|
+
path.join(EXPORT_ROOT, record.relativePath),
|
|
573
|
+
{ ...record, content: record.Content }
|
|
574
|
+
);
|
|
385
575
|
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
576
|
+
// Missing files will just be written
|
|
577
|
+
if (!['in_sync', 'missing'].includes(status.status)) {
|
|
578
|
+
issues.push({ relativePath: record.relativePath, status: status.status });
|
|
579
|
+
logger.info('Conflict detected', { file: record.relativePath, status: status.status });
|
|
580
|
+
}
|
|
581
|
+
} catch (err) {
|
|
582
|
+
processingErrors.push({
|
|
583
|
+
stage: 'compare-files',
|
|
584
|
+
file: record?.relativePath || record?.Name,
|
|
585
|
+
error: err.message
|
|
586
|
+
});
|
|
587
|
+
logger.error(`Error comparing file ${record?.relativePath}`, err);
|
|
390
588
|
}
|
|
391
|
-
} catch (err) {
|
|
392
|
-
processingErrors.push({
|
|
393
|
-
stage: 'compare-files',
|
|
394
|
-
file: record?.relativePath || record?.Name,
|
|
395
|
-
error: err.message
|
|
396
|
-
});
|
|
397
|
-
logger.error(`Error comparing file ${record?.relativePath}`, err);
|
|
398
|
-
}
|
|
399
589
|
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
590
|
+
processedCount++;
|
|
591
|
+
if (processedCount % 10 === 0 || i === allRecords.length - 1) {
|
|
592
|
+
progress.updateProgress('process', processedCount, totalToProcess, `Comparing files...`);
|
|
593
|
+
}
|
|
403
594
|
}
|
|
404
|
-
|
|
405
|
-
logger.info(`Completed file comparison - found ${issues.length} conflicts`);
|
|
595
|
+
logger.info(`Completed file comparison - found ${issues.length} conflicts`);
|
|
406
596
|
|
|
407
|
-
|
|
408
|
-
|
|
597
|
+
return { allRecords, issues, deletionLogs, processingErrors };
|
|
598
|
+
})();
|
|
409
599
|
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
600
|
+
// Show completion message with error count if any
|
|
601
|
+
if (processingErrors.length > 0) {
|
|
602
|
+
progress.completeStep('process', `⚠ Processed ${allRecords.length} records with ${processingErrors.length} errors`);
|
|
603
|
+
} else {
|
|
604
|
+
progress.completeStep('process', `✓ Processed ${allRecords.length} records, ${deletionLogs.length} deletions`);
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
// Note: Error preview will be shown in final summary, not here to avoid UI glitches
|
|
608
|
+
// Pause progress display for deletion logs and conflict resolution
|
|
609
|
+
progress.stopSpinner();
|
|
416
610
|
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
console.log(chalk.gray(` 🗑️ Removed deleted ${log.entityType}: ${path.relative(process.cwd(), log.path)}`));
|
|
429
|
-
} else if (log.type === 'error') {
|
|
430
|
-
console.warn(chalk.yellow(` ⚠️ Could not delete ${log.path}: ${log.error}`));
|
|
611
|
+
// Display deletion logs
|
|
612
|
+
for (const log of deletionLogs) {
|
|
613
|
+
if (log.type === 'folder') {
|
|
614
|
+
console.log(chalk.gray(` 🗑️ Removed deleted folder: ${path.relative(process.cwd(), log.path)}`));
|
|
615
|
+
} else if (log.type === 'file') {
|
|
616
|
+
console.log(chalk.gray(` 🗑️ Removed deleted file: ${path.relative(process.cwd(), log.path)}`));
|
|
617
|
+
} else if (log.type === 'entity') {
|
|
618
|
+
console.log(chalk.gray(` 🗑️ Removed deleted ${log.entityType}: ${path.relative(process.cwd(), log.path)}`));
|
|
619
|
+
} else if (log.type === 'error') {
|
|
620
|
+
console.warn(chalk.yellow(` ⚠️ Could not delete ${log.path}: ${log.error}`));
|
|
621
|
+
}
|
|
431
622
|
}
|
|
432
|
-
}
|
|
433
623
|
|
|
434
|
-
|
|
624
|
+
let resolutionMethod = 'skip';
|
|
435
625
|
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
626
|
+
// Check for conflicts
|
|
627
|
+
progress.startStep('conflicts');
|
|
628
|
+
if (issues.length > 0) {
|
|
629
|
+
progress.stopSpinner();
|
|
630
|
+
resolutionMethod = await promptConflictResolution(issues);
|
|
631
|
+
progress.startSpinner();
|
|
632
|
+
}
|
|
633
|
+
progress.completeStep('conflicts', issues.length > 0 ? `✓ Resolved ${issues.length} conflicts` : '✓ No conflicts');
|
|
444
634
|
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
635
|
+
// Step 6: Write all ActiveClass and ActivePage records
|
|
636
|
+
progress.startStep('write');
|
|
637
|
+
await writeRecords(allRecords, resolutionMethod, progress, logger);
|
|
638
|
+
progress.completeStep('write', `✓ Wrote ${allRecords.length} files`);
|
|
449
639
|
|
|
450
640
|
// Step 7: Finish progress tracker
|
|
451
641
|
logger.info('Pull completed successfully');
|
|
@@ -456,6 +646,9 @@ export const pull = async () => {
|
|
|
456
646
|
console.log(chalk.bold(`Summary:`));
|
|
457
647
|
console.log(` • ${activeClassResult.Records.length} ActiveClass records`);
|
|
458
648
|
console.log(` • ${activePageResult.Records.length} ActivePage records`);
|
|
649
|
+
if (irisApps.length > 0) {
|
|
650
|
+
console.log(` • ${irisApps.length} Iris apps`);
|
|
651
|
+
}
|
|
459
652
|
console.log(` • ${deletionLogs.length} deletions`);
|
|
460
653
|
console.log(`📁 Saved to: ./${EXPORT_ROOT}/`);
|
|
461
654
|
|