@dboio/cli 0.20.0 → 0.20.4
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/package.json +1 -1
- package/plugins/claude/dbo/.claude-plugin/plugin.json +1 -1
- package/plugins/claude/dbo/docs/dbo-cli-readme.md +234 -64
- package/plugins/claude/dbo/docs/dual-platform-maintenance.md +135 -0
- package/plugins/claude/track/.claude-plugin/plugin.json +1 -1
- package/src/commands/adopt.js +18 -16
- package/src/commands/clone.js +172 -7
- package/src/commands/push.js +65 -26
- package/src/lib/config.js +26 -0
- package/src/lib/dependencies.js +11 -9
- package/src/lib/input-parser.js +2 -6
- package/src/lib/insert.js +5 -4
- package/src/lib/ticketing.js +57 -1
- package/src/lib/toe-stepping.js +103 -3
package/src/lib/toe-stepping.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import { dirname, basename, join } from 'path';
|
|
3
|
-
import { readFile } from 'fs/promises';
|
|
4
|
-
import { findBaselineEntry, shouldSkipColumn, normalizeValue, isReference, resolveReferencePath } from './delta.js';
|
|
3
|
+
import { readFile, writeFile } from 'fs/promises';
|
|
4
|
+
import { findBaselineEntry, shouldSkipColumn, normalizeValue, isReference, resolveReferencePath, saveBaseline } from './delta.js';
|
|
5
5
|
import { resolveContentValue } from '../commands/clone.js';
|
|
6
6
|
import { computeLineDiff, formatDiff } from './diff.js';
|
|
7
7
|
import { parseMetaFilename } from './filenames.js';
|
|
8
|
-
import { parseServerDate } from './timestamps.js';
|
|
8
|
+
import { parseServerDate, setFileTimestamps } from './timestamps.js';
|
|
9
9
|
import { log } from './logger.js';
|
|
10
10
|
|
|
11
11
|
/**
|
|
@@ -335,6 +335,7 @@ export async function checkToeStepping(records, client, baseline, options, appSh
|
|
|
335
335
|
let skippedUIDs = new Set();
|
|
336
336
|
let bulkAction = null; // 'push_all' | 'skip_all'
|
|
337
337
|
let hasConflicts = false;
|
|
338
|
+
let baselineModified = false;
|
|
338
339
|
|
|
339
340
|
for (const { meta, metaPath } of records) {
|
|
340
341
|
const uid = meta.UID;
|
|
@@ -381,6 +382,14 @@ export async function checkToeStepping(records, client, baseline, options, appSh
|
|
|
381
382
|
if (options.yes || bulkAction === 'push_all') {
|
|
382
383
|
continue; // push this record
|
|
383
384
|
}
|
|
385
|
+
if (bulkAction === 'pull_all') {
|
|
386
|
+
await applyServerToLocal(serverEntry, meta, metaPath, serverTz);
|
|
387
|
+
_updateBaselineEntry(baseline, entity, uid, serverEntry);
|
|
388
|
+
baselineModified = true;
|
|
389
|
+
log.success(` Pulled server version of "${label}" to local`);
|
|
390
|
+
skippedUIDs.add(uid);
|
|
391
|
+
continue;
|
|
392
|
+
}
|
|
384
393
|
if (bulkAction === 'skip_all') {
|
|
385
394
|
skippedUIDs.add(uid);
|
|
386
395
|
continue;
|
|
@@ -395,9 +404,11 @@ export async function checkToeStepping(records, client, baseline, options, appSh
|
|
|
395
404
|
message: `"${label}" has server changes. How to proceed?`,
|
|
396
405
|
choices: [
|
|
397
406
|
{ name: 'Push anyway (overwrite server changes)', value: 'push' },
|
|
407
|
+
{ name: 'Pull from server (overwrite local changes)', value: 'pull' },
|
|
398
408
|
{ name: 'Compare differences', value: 'compare' },
|
|
399
409
|
{ name: 'Skip this record', value: 'skip' },
|
|
400
410
|
{ name: 'Push all remaining (overwrite all)', value: 'push_all' },
|
|
411
|
+
{ name: 'Pull all remaining (overwrite all local)', value: 'pull_all' },
|
|
401
412
|
{ name: 'Skip all remaining', value: 'skip_all' },
|
|
402
413
|
{ name: 'Cancel entire push', value: 'cancel' },
|
|
403
414
|
],
|
|
@@ -414,6 +425,14 @@ export async function checkToeStepping(records, client, baseline, options, appSh
|
|
|
414
425
|
log.info('Push cancelled. Run "dbo pull" to fetch server changes first.');
|
|
415
426
|
return false;
|
|
416
427
|
}
|
|
428
|
+
if (action === 'pull' || action === 'pull_all') {
|
|
429
|
+
await applyServerToLocal(serverEntry, meta, metaPath, serverTz);
|
|
430
|
+
_updateBaselineEntry(baseline, entity, uid, serverEntry);
|
|
431
|
+
baselineModified = true;
|
|
432
|
+
log.success(` Pulled server version of "${label}" to local`);
|
|
433
|
+
skippedUIDs.add(uid);
|
|
434
|
+
if (action === 'pull_all') bulkAction = 'pull_all';
|
|
435
|
+
}
|
|
417
436
|
if (action === 'skip' || action === 'skip_all') {
|
|
418
437
|
skippedUIDs.add(uid);
|
|
419
438
|
if (action === 'skip_all') bulkAction = 'skip_all';
|
|
@@ -423,6 +442,12 @@ export async function checkToeStepping(records, client, baseline, options, appSh
|
|
|
423
442
|
}
|
|
424
443
|
}
|
|
425
444
|
|
|
445
|
+
if (baselineModified) {
|
|
446
|
+
try {
|
|
447
|
+
await saveBaseline(baseline);
|
|
448
|
+
} catch { /* non-critical — next push will re-detect */ }
|
|
449
|
+
}
|
|
450
|
+
|
|
426
451
|
if (!hasConflicts) return true;
|
|
427
452
|
|
|
428
453
|
// Return skipped UIDs so the caller can filter them out
|
|
@@ -496,3 +521,78 @@ async function showPushDiff(serverEntry, localMeta, metaPath) {
|
|
|
496
521
|
|
|
497
522
|
log.plain('');
|
|
498
523
|
}
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* Update the in-memory baseline entry for a record with the server's current
|
|
527
|
+
* values. Called after pulling from server so the next toe-stepping check
|
|
528
|
+
* sees the pulled state as the new baseline and does not re-raise the conflict.
|
|
529
|
+
*
|
|
530
|
+
* @param {Object} baseline - Loaded baseline object (mutated in place)
|
|
531
|
+
* @param {string} entity - Entity type (e.g., "content")
|
|
532
|
+
* @param {string} uid - Record UID
|
|
533
|
+
* @param {Object} serverEntry - Live server record
|
|
534
|
+
*/
|
|
535
|
+
function _updateBaselineEntry(baseline, entity, uid, serverEntry) {
|
|
536
|
+
if (!baseline?.children) return;
|
|
537
|
+
const arr = baseline.children[entity];
|
|
538
|
+
if (!Array.isArray(arr)) return;
|
|
539
|
+
const idx = arr.findIndex(e => e.UID === uid);
|
|
540
|
+
if (idx < 0) return;
|
|
541
|
+
|
|
542
|
+
const SKIP = new Set(['_entity', '_companionReferenceColumns', '_contentColumns',
|
|
543
|
+
'_mediaFile', '_pathConfirmed', 'children', '_id']);
|
|
544
|
+
for (const [col, rawVal] of Object.entries(serverEntry)) {
|
|
545
|
+
if (SKIP.has(col)) continue;
|
|
546
|
+
const decoded = resolveContentValue(rawVal);
|
|
547
|
+
arr[idx][col] = decoded !== null ? decoded : rawVal;
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/**
|
|
552
|
+
* Overwrite local metadata and companion content files with server values.
|
|
553
|
+
*
|
|
554
|
+
* Called when the user chooses "Pull from server" during conflict resolution.
|
|
555
|
+
* Updates:
|
|
556
|
+
* - companion content files (columns listed in _companionReferenceColumns)
|
|
557
|
+
* - all non-system metadata fields
|
|
558
|
+
* - _LastUpdated / _CreatedOn timestamps in the metadata JSON
|
|
559
|
+
* - file timestamps on both the metadata file and any companion files
|
|
560
|
+
*
|
|
561
|
+
* @param {Object} serverEntry - Live server record (from fetchServerRecord*)
|
|
562
|
+
* @param {Object} localMeta - Currently loaded metadata object
|
|
563
|
+
* @param {string} metaPath - Absolute path to the .metadata.json file
|
|
564
|
+
* @param {string} [serverTz] - Server timezone string (e.g. "America/Chicago")
|
|
565
|
+
*/
|
|
566
|
+
async function applyServerToLocal(serverEntry, localMeta, metaPath, serverTz) {
|
|
567
|
+
const metaDir = dirname(metaPath);
|
|
568
|
+
const companions = new Set(localMeta._companionReferenceColumns || []);
|
|
569
|
+
|
|
570
|
+
// Write companion content files from server values
|
|
571
|
+
for (const col of companions) {
|
|
572
|
+
const ref = localMeta[col];
|
|
573
|
+
if (!ref || !String(ref).startsWith('@')) continue;
|
|
574
|
+
const filePath = join(metaDir, String(ref).substring(1));
|
|
575
|
+
const serverValue = resolveContentValue(serverEntry[col]);
|
|
576
|
+
if (serverValue !== null) {
|
|
577
|
+
await writeFile(filePath, serverValue, 'utf8');
|
|
578
|
+
try {
|
|
579
|
+
await setFileTimestamps(filePath, serverEntry._CreatedOn, serverEntry._LastUpdated, serverTz);
|
|
580
|
+
} catch { /* non-critical */ }
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// Merge non-system, non-companion server columns into localMeta
|
|
585
|
+
const skipMeta = new Set(['_entity', '_companionReferenceColumns', '_contentColumns', '_mediaFile',
|
|
586
|
+
'_pathConfirmed', 'children', '_id']);
|
|
587
|
+
for (const [col, rawVal] of Object.entries(serverEntry)) {
|
|
588
|
+
if (skipMeta.has(col)) continue;
|
|
589
|
+
if (companions.has(col)) continue; // companion already handled as a file
|
|
590
|
+
const decoded = resolveContentValue(rawVal);
|
|
591
|
+
localMeta[col] = decoded !== null ? decoded : rawVal;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
await writeFile(metaPath, JSON.stringify(localMeta, null, 2) + '\n');
|
|
595
|
+
try {
|
|
596
|
+
await setFileTimestamps(metaPath, serverEntry._CreatedOn, serverEntry._LastUpdated, serverTz);
|
|
597
|
+
} catch { /* non-critical */ }
|
|
598
|
+
}
|