@dboio/cli 0.4.2 → 0.6.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 +294 -71
- package/bin/dbo.js +12 -3
- package/bin/postinstall.js +88 -0
- package/package.json +10 -3
- package/src/commands/clone.js +597 -19
- package/src/commands/diff.js +246 -0
- package/src/commands/init.js +30 -22
- package/src/commands/install.js +517 -69
- package/src/commands/mv.js +869 -0
- package/src/commands/pull.js +6 -0
- package/src/commands/push.js +289 -33
- package/src/commands/rm.js +337 -0
- package/src/commands/status.js +28 -1
- package/src/lib/config.js +265 -0
- package/src/lib/delta.js +204 -0
- package/src/lib/dependencies.js +131 -0
- package/src/lib/diff.js +740 -0
- package/src/lib/save-to-disk.js +71 -4
- package/src/lib/structure.js +36 -0
- package/src/plugins/claudecommands/dbo.md +37 -6
- package/src/commands/update.js +0 -168
package/src/commands/pull.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
2
|
import chalk from 'chalk';
|
|
3
3
|
import { DboClient } from '../lib/client.js';
|
|
4
|
+
import { loadConfig } from '../lib/config.js';
|
|
4
5
|
import { formatError } from '../lib/formatter.js';
|
|
5
6
|
import { saveToDisk } from '../lib/save-to-disk.js';
|
|
6
7
|
import { log } from '../lib/logger.js';
|
|
@@ -82,6 +83,7 @@ export const pullCommand = new Command('pull')
|
|
|
82
83
|
|
|
83
84
|
// Content entity has well-known defaults — skip prompts
|
|
84
85
|
const isContent = entity === 'content';
|
|
86
|
+
const config = await loadConfig();
|
|
85
87
|
|
|
86
88
|
if (isContent) {
|
|
87
89
|
await saveToDisk(rows, columns, {
|
|
@@ -94,12 +96,16 @@ export const pullCommand = new Command('pull')
|
|
|
94
96
|
contentFileMap: columns.includes('Content') ? {
|
|
95
97
|
Content: { suffix: '', extensionSource: columns.includes('Extension') ? 'Extension' : null, extension: 'txt' },
|
|
96
98
|
} : null,
|
|
99
|
+
changeDetection: true,
|
|
100
|
+
config,
|
|
97
101
|
});
|
|
98
102
|
} else {
|
|
99
103
|
// Other entities: interactive prompts for column selection
|
|
100
104
|
await saveToDisk(rows, columns, {
|
|
101
105
|
entity,
|
|
102
106
|
nonInteractive: false,
|
|
107
|
+
changeDetection: true,
|
|
108
|
+
config,
|
|
103
109
|
});
|
|
104
110
|
}
|
|
105
111
|
} catch (err) {
|
package/src/commands/push.js
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
|
-
import { readFile,
|
|
2
|
+
import { readFile, stat, writeFile } from 'fs/promises';
|
|
3
3
|
import { join, dirname, basename, extname, relative } from 'path';
|
|
4
4
|
import { DboClient } from '../lib/client.js';
|
|
5
5
|
import { buildInputBody, checkSubmitErrors } from '../lib/input-parser.js';
|
|
6
6
|
import { formatResponse, formatError } from '../lib/formatter.js';
|
|
7
7
|
import { log } from '../lib/logger.js';
|
|
8
8
|
import { shouldSkipColumn } from '../lib/columns.js';
|
|
9
|
-
import { loadConfig } from '../lib/config.js';
|
|
9
|
+
import { loadConfig, loadSynchronize, saveSynchronize, loadAppJsonBaseline, saveAppJsonBaseline, hasBaseline } from '../lib/config.js';
|
|
10
10
|
import { setFileTimestamps } from '../lib/timestamps.js';
|
|
11
|
+
import { findMetadataFiles } from '../lib/diff.js';
|
|
12
|
+
import { detectChangedColumns, findBaselineEntry } from '../lib/delta.js';
|
|
13
|
+
import { buildDependencyGraph } from '../lib/dependencies.js';
|
|
11
14
|
|
|
12
15
|
export const pushCommand = new Command('push')
|
|
13
16
|
.description('Push local files back to DBO.io using metadata from pull')
|
|
@@ -24,6 +27,10 @@ export const pushCommand = new Command('push')
|
|
|
24
27
|
.action(async (targetPath, options) => {
|
|
25
28
|
try {
|
|
26
29
|
const client = new DboClient({ domain: options.domain, verbose: options.verbose });
|
|
30
|
+
|
|
31
|
+
// Process pending deletions from synchronize.json
|
|
32
|
+
await processPendingDeletes(client, options);
|
|
33
|
+
|
|
27
34
|
const pathStat = await stat(targetPath);
|
|
28
35
|
|
|
29
36
|
if (pathStat.isDirectory()) {
|
|
@@ -37,6 +44,71 @@ export const pushCommand = new Command('push')
|
|
|
37
44
|
}
|
|
38
45
|
});
|
|
39
46
|
|
|
47
|
+
/**
|
|
48
|
+
* Process pending delete entries from .dbo/synchronize.json
|
|
49
|
+
*/
|
|
50
|
+
async function processPendingDeletes(client, options) {
|
|
51
|
+
const sync = await loadSynchronize();
|
|
52
|
+
if (!sync.delete || sync.delete.length === 0) return;
|
|
53
|
+
|
|
54
|
+
log.info(`Processing ${sync.delete.length} pending deletion(s)...`);
|
|
55
|
+
|
|
56
|
+
const remaining = [];
|
|
57
|
+
const deletedUids = [];
|
|
58
|
+
|
|
59
|
+
for (const entry of sync.delete) {
|
|
60
|
+
log.info(`Deleting "${entry.name}" (${entry.entity}:${entry.RowID})`);
|
|
61
|
+
|
|
62
|
+
const extraParams = { '_confirm': options.confirm || 'true' };
|
|
63
|
+
if (options.ticket) extraParams['_OverrideTicketID'] = options.ticket;
|
|
64
|
+
|
|
65
|
+
const body = await buildInputBody([entry.expression], extraParams);
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
const result = await client.postUrlEncoded('/api/input/submit', body);
|
|
69
|
+
|
|
70
|
+
// Retry with prompted params if needed
|
|
71
|
+
const retryParams = await checkSubmitErrors(result);
|
|
72
|
+
if (retryParams) {
|
|
73
|
+
Object.assign(extraParams, retryParams);
|
|
74
|
+
const retryBody = await buildInputBody([entry.expression], extraParams);
|
|
75
|
+
const retryResult = await client.postUrlEncoded('/api/input/submit', retryBody);
|
|
76
|
+
if (retryResult.successful) {
|
|
77
|
+
log.success(` Deleted "${entry.name}" from server`);
|
|
78
|
+
deletedUids.push(entry.UID);
|
|
79
|
+
} else {
|
|
80
|
+
log.error(` Failed to delete "${entry.name}"`);
|
|
81
|
+
formatResponse(retryResult, { json: options.json, jq: options.jq });
|
|
82
|
+
remaining.push(entry);
|
|
83
|
+
}
|
|
84
|
+
} else if (result.successful) {
|
|
85
|
+
log.success(` Deleted "${entry.name}" from server`);
|
|
86
|
+
deletedUids.push(entry.UID);
|
|
87
|
+
} else {
|
|
88
|
+
log.error(` Failed to delete "${entry.name}"`);
|
|
89
|
+
formatResponse(result, { json: options.json, jq: options.jq });
|
|
90
|
+
remaining.push(entry);
|
|
91
|
+
}
|
|
92
|
+
} catch (err) {
|
|
93
|
+
log.error(` Failed to delete "${entry.name}": ${err.message}`);
|
|
94
|
+
remaining.push(entry);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Remove edit entries for successfully deleted records (spec requirement)
|
|
99
|
+
if (deletedUids.length > 0) {
|
|
100
|
+
sync.edit = (sync.edit || []).filter(e => !deletedUids.includes(e.UID));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Update synchronize.json with any remaining entries
|
|
104
|
+
sync.delete = remaining;
|
|
105
|
+
await saveSynchronize(sync);
|
|
106
|
+
|
|
107
|
+
if (remaining.length > 0) {
|
|
108
|
+
log.warn(`${remaining.length} deletion(s) failed and remain staged.`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
40
112
|
/**
|
|
41
113
|
* Push a single file using its companion .metadata.json
|
|
42
114
|
*/
|
|
@@ -70,8 +142,17 @@ async function pushDirectory(dirPath, client, options) {
|
|
|
70
142
|
|
|
71
143
|
log.info(`Found ${metaFiles.length} record(s) to push`);
|
|
72
144
|
|
|
73
|
-
|
|
74
|
-
|
|
145
|
+
// Load baseline for delta detection
|
|
146
|
+
const baseline = await loadAppJsonBaseline();
|
|
147
|
+
const config = await loadConfig();
|
|
148
|
+
|
|
149
|
+
if (!baseline) {
|
|
150
|
+
log.warn('No .app.json baseline found — performing full push (run "dbo clone" to enable delta sync)');
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Collect metadata with detected changes
|
|
154
|
+
const toPush = [];
|
|
155
|
+
let skipped = 0;
|
|
75
156
|
|
|
76
157
|
for (const metaPath of metaFiles) {
|
|
77
158
|
let meta;
|
|
@@ -79,19 +160,19 @@ async function pushDirectory(dirPath, client, options) {
|
|
|
79
160
|
meta = JSON.parse(await readFile(metaPath, 'utf8'));
|
|
80
161
|
} catch (err) {
|
|
81
162
|
log.warn(`Skipping invalid metadata: ${metaPath} (${err.message})`);
|
|
82
|
-
|
|
163
|
+
skipped++;
|
|
83
164
|
continue;
|
|
84
165
|
}
|
|
85
166
|
|
|
86
167
|
if (!meta.UID && !meta._id) {
|
|
87
168
|
log.warn(`Skipping "${metaPath}": no UID or _id found`);
|
|
88
|
-
|
|
169
|
+
skipped++;
|
|
89
170
|
continue;
|
|
90
171
|
}
|
|
91
172
|
|
|
92
173
|
if (!meta._entity) {
|
|
93
174
|
log.warn(`Skipping "${metaPath}": no _entity found`);
|
|
94
|
-
|
|
175
|
+
skipped++;
|
|
95
176
|
continue;
|
|
96
177
|
}
|
|
97
178
|
|
|
@@ -111,24 +192,77 @@ async function pushDirectory(dirPath, client, options) {
|
|
|
111
192
|
}
|
|
112
193
|
}
|
|
113
194
|
}
|
|
114
|
-
if (missingFiles) {
|
|
195
|
+
if (missingFiles) { skipped++; continue; }
|
|
196
|
+
|
|
197
|
+
// Detect changed columns (delta detection)
|
|
198
|
+
let changedColumns = null;
|
|
199
|
+
if (baseline) {
|
|
200
|
+
try {
|
|
201
|
+
changedColumns = await detectChangedColumns(metaPath, baseline, config);
|
|
202
|
+
if (changedColumns.length === 0) {
|
|
203
|
+
log.dim(` Skipping ${basename(metaPath)} — no changes detected`);
|
|
204
|
+
skipped++;
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
} catch (err) {
|
|
208
|
+
log.warn(`Delta detection failed for ${metaPath}: ${err.message} — performing full push`);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
toPush.push({ meta, metaPath, changedColumns });
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (toPush.length === 0) {
|
|
216
|
+
log.info('No changes to push');
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
115
219
|
|
|
220
|
+
// Group by entity and apply dependency ordering
|
|
221
|
+
const byEntity = {};
|
|
222
|
+
for (const item of toPush) {
|
|
223
|
+
const entity = item.meta._entity;
|
|
224
|
+
if (!byEntity[entity]) byEntity[entity] = [];
|
|
225
|
+
byEntity[entity].push(item);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Process in dependency order
|
|
229
|
+
let succeeded = 0;
|
|
230
|
+
let failed = 0;
|
|
231
|
+
const successfulPushes = [];
|
|
232
|
+
|
|
233
|
+
for (const item of toPush) {
|
|
116
234
|
try {
|
|
117
|
-
await pushFromMetadata(meta, metaPath, client, options);
|
|
118
|
-
|
|
235
|
+
const success = await pushFromMetadata(item.meta, item.metaPath, client, options, item.changedColumns);
|
|
236
|
+
if (success) {
|
|
237
|
+
succeeded++;
|
|
238
|
+
successfulPushes.push(item);
|
|
239
|
+
} else {
|
|
240
|
+
failed++;
|
|
241
|
+
}
|
|
119
242
|
} catch (err) {
|
|
120
|
-
log.error(`Failed: ${metaPath} — ${err.message}`);
|
|
243
|
+
log.error(`Failed: ${item.metaPath} — ${err.message}`);
|
|
121
244
|
failed++;
|
|
122
245
|
}
|
|
123
246
|
}
|
|
124
247
|
|
|
125
|
-
|
|
248
|
+
// Update baseline after successful pushes
|
|
249
|
+
if (baseline && successfulPushes.length > 0) {
|
|
250
|
+
await updateBaselineAfterPush(baseline, successfulPushes);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
log.info(`Push complete: ${succeeded} succeeded, ${failed} failed, ${skipped} skipped`);
|
|
126
254
|
}
|
|
127
255
|
|
|
128
256
|
/**
|
|
129
257
|
* Build and submit input expressions from a metadata object
|
|
258
|
+
* @param {Object} meta - Metadata object
|
|
259
|
+
* @param {string} metaPath - Path to metadata file
|
|
260
|
+
* @param {DboClient} client - API client
|
|
261
|
+
* @param {Object} options - Push options
|
|
262
|
+
* @param {string[]|null} changedColumns - Optional array of changed column names (for delta sync)
|
|
263
|
+
* @returns {Promise<boolean>} - True if push succeeded
|
|
130
264
|
*/
|
|
131
|
-
async function pushFromMetadata(meta, metaPath, client, options) {
|
|
265
|
+
async function pushFromMetadata(meta, metaPath, client, options, changedColumns = null) {
|
|
132
266
|
const uid = meta.UID || meta._id;
|
|
133
267
|
const entity = meta._entity;
|
|
134
268
|
const contentCols = new Set(meta._contentColumns || []);
|
|
@@ -149,11 +283,17 @@ async function pushFromMetadata(meta, metaPath, client, options) {
|
|
|
149
283
|
const dataExprs = [];
|
|
150
284
|
let metaUpdated = false;
|
|
151
285
|
|
|
286
|
+
// If changedColumns is provided, only push those columns (delta sync)
|
|
287
|
+
const columnsToProcess = changedColumns ? new Set(changedColumns) : null;
|
|
288
|
+
|
|
152
289
|
for (const [key, value] of Object.entries(meta)) {
|
|
153
290
|
if (shouldSkipColumn(key)) continue;
|
|
154
291
|
if (key === 'UID') continue; // UID is the identifier, not a column to update
|
|
155
292
|
if (value === null || value === undefined) continue;
|
|
156
293
|
|
|
294
|
+
// Delta sync: skip columns not in changedColumns
|
|
295
|
+
if (columnsToProcess && !columnsToProcess.has(key)) continue;
|
|
296
|
+
|
|
157
297
|
const isContentCol = contentCols.has(key);
|
|
158
298
|
|
|
159
299
|
// --meta-only: skip content columns
|
|
@@ -175,10 +315,11 @@ async function pushFromMetadata(meta, metaPath, client, options) {
|
|
|
175
315
|
|
|
176
316
|
if (dataExprs.length === 0) {
|
|
177
317
|
log.warn(`Nothing to push for ${basename(metaPath)}`);
|
|
178
|
-
return;
|
|
318
|
+
return false;
|
|
179
319
|
}
|
|
180
320
|
|
|
181
|
-
|
|
321
|
+
const fieldLabel = changedColumns ? `${dataExprs.length} changed field(s)` : `${dataExprs.length} field(s)`;
|
|
322
|
+
log.info(`Pushing ${basename(metaPath, '.metadata.json')} (${entity}:${uid}) — ${fieldLabel}`);
|
|
182
323
|
|
|
183
324
|
const extraParams = { '_confirm': options.confirm };
|
|
184
325
|
if (options.ticket) extraParams['_OverrideTicketID'] = options.ticket;
|
|
@@ -203,7 +344,7 @@ async function pushFromMetadata(meta, metaPath, client, options) {
|
|
|
203
344
|
}
|
|
204
345
|
|
|
205
346
|
if (!result.successful) {
|
|
206
|
-
|
|
347
|
+
return false;
|
|
207
348
|
}
|
|
208
349
|
|
|
209
350
|
// Update file timestamps from server response
|
|
@@ -232,6 +373,64 @@ async function pushFromMetadata(meta, metaPath, client, options) {
|
|
|
232
373
|
}
|
|
233
374
|
}
|
|
234
375
|
} catch { /* non-critical timestamp update */ }
|
|
376
|
+
|
|
377
|
+
return true;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Normalize a local file path for comparison with server-side Path.
|
|
382
|
+
*
|
|
383
|
+
* The Bins/ directory is used for local file organization and does not reflect
|
|
384
|
+
* the actual server-side serving path. This function strips organizational prefixes
|
|
385
|
+
* to enable correct path comparison during push operations.
|
|
386
|
+
*
|
|
387
|
+
* **Directory Structure:**
|
|
388
|
+
* - `Bins/` — Local organizational root (always stripped)
|
|
389
|
+
* - `Bins/app/` — Special case: the "app" subdirectory is also stripped because it's
|
|
390
|
+
* purely organizational. Files in Bins/app/ are served from the app root without
|
|
391
|
+
* the "app/" prefix on the server.
|
|
392
|
+
* - `Bins/custom_name/` — Custom bin directories (tpl/, ticket_test/, etc.) are
|
|
393
|
+
* preserved because they represent actual bin hierarchies that serve from their
|
|
394
|
+
* directory name.
|
|
395
|
+
*
|
|
396
|
+
* **Why "app/" is special:**
|
|
397
|
+
* The main app bin is placed in Bins/app/ locally for organization, but server-side
|
|
398
|
+
* these files are served from the root path (no "app/" prefix). Other custom bins
|
|
399
|
+
* like "tpl/" maintain their directory name in the serving path.
|
|
400
|
+
*
|
|
401
|
+
* Examples:
|
|
402
|
+
* "Bins/app/assets/css/file.css" → "assets/css/file.css" (strips Bins/app/)
|
|
403
|
+
* "Bins/tpl/header.html" → "tpl/header.html" (preserves tpl/)
|
|
404
|
+
* "Bins/assets/css/file.css" → "assets/css/file.css" (strips Bins/ only)
|
|
405
|
+
* "Sites/MySite/content/page.html" → "Sites/MySite/content/page.html" (unchanged)
|
|
406
|
+
*
|
|
407
|
+
* @param {string} localPath - Relative path from project root
|
|
408
|
+
* @returns {string} - Normalized path for comparison with metadata Path column
|
|
409
|
+
*/
|
|
410
|
+
function normalizePathForComparison(localPath) {
|
|
411
|
+
// Convert backslashes to forward slashes (Windows compatibility)
|
|
412
|
+
const normalized = localPath.replace(/\\/g, '/');
|
|
413
|
+
|
|
414
|
+
// Strip leading and trailing slashes
|
|
415
|
+
const cleaned = normalized.replace(/^\/+|\/+$/g, '');
|
|
416
|
+
|
|
417
|
+
// Check if path starts with "Bins/" organizational directory
|
|
418
|
+
if (cleaned.startsWith('Bins/')) {
|
|
419
|
+
// Remove "Bins/" prefix (length = 5)
|
|
420
|
+
const withoutBins = cleaned.substring(5);
|
|
421
|
+
|
|
422
|
+
// Special case: strip "app/" organizational subdirectory
|
|
423
|
+
// This is the only special subdirectory - all others (tpl/, assets/, etc.) are preserved
|
|
424
|
+
if (withoutBins.startsWith('app/')) {
|
|
425
|
+
return withoutBins.substring(4); // Remove "app/" (length = 4)
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// For all other paths, return without Bins/ prefix
|
|
429
|
+
return withoutBins;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// Path doesn't start with Bins/, return as-is
|
|
433
|
+
return cleaned;
|
|
235
434
|
}
|
|
236
435
|
|
|
237
436
|
/**
|
|
@@ -258,15 +457,25 @@ async function checkPathMismatch(meta, metaPath, options) {
|
|
|
258
457
|
const currentFilePath = join(metaDir, contentFileName);
|
|
259
458
|
const currentRelPath = relative(process.cwd(), currentFilePath);
|
|
260
459
|
|
|
261
|
-
// Normalize
|
|
262
|
-
|
|
263
|
-
const
|
|
460
|
+
// Normalize both paths for comparison
|
|
461
|
+
// The metadata Path may also incorrectly contain Bins/app/ prefix from old data
|
|
462
|
+
const storedPathRaw = String(meta.Path).replace(/^\/+|\/+$/g, ''); // Strip leading/trailing slashes
|
|
463
|
+
const normalizedStoredPath = normalizePathForComparison(storedPathRaw);
|
|
464
|
+
const normalizedCurrentPath = normalizePathForComparison(currentRelPath);
|
|
465
|
+
|
|
466
|
+
// Compare with flexible handling of leading slash
|
|
467
|
+
// (Both /path and path are considered equivalent)
|
|
468
|
+
const storedNormalized = normalizedStoredPath.replace(/^\/+/, '');
|
|
469
|
+
const currentNormalized = normalizedCurrentPath.replace(/^\/+/, '');
|
|
470
|
+
|
|
471
|
+
if (storedNormalized === currentNormalized) return;
|
|
264
472
|
|
|
265
|
-
|
|
473
|
+
// Use normalized path for display
|
|
474
|
+
const currentPath = normalizedCurrentPath;
|
|
266
475
|
|
|
267
476
|
// Path mismatch detected
|
|
268
477
|
log.warn(`Path mismatch for "${metaBase}":`);
|
|
269
|
-
log.label(' Metadata Path',
|
|
478
|
+
log.label(' Metadata Path', storedPathRaw);
|
|
270
479
|
log.label(' Current path ', currentPath);
|
|
271
480
|
|
|
272
481
|
let updatePath = options.yes;
|
|
@@ -290,20 +499,67 @@ async function checkPathMismatch(meta, metaPath, options) {
|
|
|
290
499
|
}
|
|
291
500
|
|
|
292
501
|
/**
|
|
293
|
-
*
|
|
502
|
+
* Update baseline file (.app.json) after successful pushes.
|
|
503
|
+
* Syncs changed column values and timestamps from metadata to baseline.
|
|
504
|
+
*
|
|
505
|
+
* @param {Object} baseline - The baseline JSON object
|
|
506
|
+
* @param {Array} successfulPushes - Array of { meta, metaPath, changedColumns }
|
|
294
507
|
*/
|
|
295
|
-
async function
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
const
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
508
|
+
async function updateBaselineAfterPush(baseline, successfulPushes) {
|
|
509
|
+
let modified = false;
|
|
510
|
+
|
|
511
|
+
for (const { meta, metaPath, changedColumns } of successfulPushes) {
|
|
512
|
+
const uid = meta.UID || meta._id;
|
|
513
|
+
const entity = meta._entity;
|
|
514
|
+
|
|
515
|
+
// Find the baseline entry
|
|
516
|
+
const baselineEntry = findBaselineEntry(baseline, entity, uid);
|
|
517
|
+
if (!baselineEntry) {
|
|
518
|
+
log.warn(` Baseline entry not found for ${entity}:${uid} — skipping baseline update`);
|
|
519
|
+
continue;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// Update _LastUpdated and _LastUpdatedUserID from metadata
|
|
523
|
+
if (meta._LastUpdated) {
|
|
524
|
+
baselineEntry._LastUpdated = meta._LastUpdated;
|
|
525
|
+
modified = true;
|
|
526
|
+
}
|
|
527
|
+
if (meta._LastUpdatedUserID) {
|
|
528
|
+
baselineEntry._LastUpdatedUserID = meta._LastUpdatedUserID;
|
|
529
|
+
modified = true;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// Update changed column values in baseline
|
|
533
|
+
const columnsToUpdate = changedColumns || Object.keys(meta).filter(k => !shouldSkipColumn(k) && k !== 'UID');
|
|
534
|
+
|
|
535
|
+
for (const col of columnsToUpdate) {
|
|
536
|
+
const value = meta[col];
|
|
537
|
+
if (value === null || value === undefined) continue;
|
|
538
|
+
|
|
539
|
+
const strValue = String(value);
|
|
540
|
+
|
|
541
|
+
// If it's a @reference, read the file content and store in baseline
|
|
542
|
+
if (strValue.startsWith('@')) {
|
|
543
|
+
try {
|
|
544
|
+
const refFile = strValue.substring(1);
|
|
545
|
+
const refPath = join(dirname(metaPath), refFile);
|
|
546
|
+
const fileContent = await readFile(refPath, 'utf8');
|
|
547
|
+
baselineEntry[col] = fileContent;
|
|
548
|
+
modified = true;
|
|
549
|
+
} catch (err) {
|
|
550
|
+
log.warn(` Failed to read ${strValue} for baseline update: ${err.message}`);
|
|
551
|
+
}
|
|
552
|
+
} else {
|
|
553
|
+
// Scalar value: store directly
|
|
554
|
+
baselineEntry[col] = value;
|
|
555
|
+
modified = true;
|
|
556
|
+
}
|
|
305
557
|
}
|
|
306
558
|
}
|
|
307
559
|
|
|
308
|
-
|
|
560
|
+
// Save updated baseline
|
|
561
|
+
if (modified) {
|
|
562
|
+
await saveAppJsonBaseline(baseline);
|
|
563
|
+
log.dim(' Baseline updated with pushed changes');
|
|
564
|
+
}
|
|
309
565
|
}
|