@dboio/cli 0.15.0 → 0.15.2
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/src/commands/push.js +69 -109
- package/src/lib/tagging.js +15 -18
package/package.json
CHANGED
package/src/commands/push.js
CHANGED
|
@@ -20,17 +20,7 @@ import { BINS_DIR, ENTITY_DIR_NAMES, loadStructureFile, findBinByPath } from '..
|
|
|
20
20
|
import { ensureTrashIcon, setFileTag } from '../lib/tagging.js';
|
|
21
21
|
import { checkToeStepping } from '../lib/toe-stepping.js';
|
|
22
22
|
import { runPendingMigrations } from '../lib/migrations.js';
|
|
23
|
-
import { findUnaddedFiles, detectBinFile, submitAdd } from './add.js';
|
|
24
|
-
|
|
25
|
-
function _getMetaCompanionPaths(meta, metaPath) {
|
|
26
|
-
const dir = dirname(metaPath);
|
|
27
|
-
const paths = [];
|
|
28
|
-
for (const col of (meta._contentColumns || [])) {
|
|
29
|
-
const ref = meta[col];
|
|
30
|
-
if (ref && String(ref).startsWith('@')) paths.push(join(dir, String(ref).substring(1)));
|
|
31
|
-
}
|
|
32
|
-
return paths;
|
|
33
|
-
}
|
|
23
|
+
// AUTO-ADD DISABLED: import { findUnaddedFiles, detectBinFile, submitAdd } from './add.js';
|
|
34
24
|
|
|
35
25
|
/**
|
|
36
26
|
* Resolve an @reference file path to an absolute filesystem path.
|
|
@@ -96,7 +86,7 @@ export const pushCommand = new Command('push')
|
|
|
96
86
|
const transactionKey = await resolveTransactionKey(options);
|
|
97
87
|
|
|
98
88
|
// Process pending deletions from synchronize.json
|
|
99
|
-
await processPendingDeletes(client, options, modifyKey, transactionKey);
|
|
89
|
+
const deletedCount = await processPendingDeletes(client, options, modifyKey, transactionKey) || 0;
|
|
100
90
|
|
|
101
91
|
// ── Resolution order ──────────────────────────────────────────
|
|
102
92
|
// 1. Commas → UID list
|
|
@@ -135,7 +125,7 @@ export const pushCommand = new Command('push')
|
|
|
135
125
|
log.dim(` Found: ${relative(process.cwd(), resolved)}`);
|
|
136
126
|
const resolvedStat = await stat(resolved);
|
|
137
127
|
if (resolvedStat.isDirectory()) {
|
|
138
|
-
await pushDirectory(resolved, client, options, modifyKey, transactionKey);
|
|
128
|
+
await pushDirectory(resolved, client, options, modifyKey, transactionKey, deletedCount);
|
|
139
129
|
} else {
|
|
140
130
|
await pushSingleFile(resolved, client, options, modifyKey, transactionKey);
|
|
141
131
|
}
|
|
@@ -156,7 +146,7 @@ export const pushCommand = new Command('push')
|
|
|
156
146
|
}
|
|
157
147
|
|
|
158
148
|
if (pathStat.isDirectory()) {
|
|
159
|
-
await pushDirectory(targetPath, client, options, modifyKey, transactionKey);
|
|
149
|
+
await pushDirectory(targetPath, client, options, modifyKey, transactionKey, deletedCount);
|
|
160
150
|
} else {
|
|
161
151
|
await pushSingleFile(targetPath, client, options, modifyKey, transactionKey);
|
|
162
152
|
}
|
|
@@ -171,18 +161,27 @@ export const pushCommand = new Command('push')
|
|
|
171
161
|
*/
|
|
172
162
|
async function processPendingDeletes(client, options, modifyKey = null, transactionKey = 'RowUID') {
|
|
173
163
|
const sync = await loadSynchronize();
|
|
174
|
-
if (!sync.delete || sync.delete.length === 0) return;
|
|
164
|
+
if (!sync.delete || sync.delete.length === 0) return 0;
|
|
175
165
|
|
|
176
166
|
log.info(`Processing ${sync.delete.length} pending deletion(s)...`);
|
|
177
167
|
|
|
178
168
|
const remaining = [];
|
|
179
169
|
const deletedUids = [];
|
|
180
170
|
|
|
171
|
+
// Load stored ticket once for all deletions (same as main push loop)
|
|
172
|
+
const { getGlobalTicket, getRecordTicket } = await import('../lib/ticketing.js');
|
|
173
|
+
const globalTicket = !options.ticket ? await getGlobalTicket() : null;
|
|
174
|
+
|
|
181
175
|
for (const entry of sync.delete) {
|
|
182
176
|
log.info(`Deleting "${entry.name}" (${entry.entity}:${entry.RowID})`);
|
|
183
177
|
|
|
184
178
|
const extraParams = { '_confirm': options.confirm || 'true' };
|
|
185
|
-
if (options.ticket)
|
|
179
|
+
if (options.ticket) {
|
|
180
|
+
extraParams['_OverrideTicketID'] = options.ticket;
|
|
181
|
+
} else {
|
|
182
|
+
const ticket = await getRecordTicket(entry.UID) || globalTicket;
|
|
183
|
+
if (ticket) extraParams['_OverrideTicketID'] = ticket;
|
|
184
|
+
}
|
|
186
185
|
if (modifyKey) extraParams['_modify_key'] = modifyKey;
|
|
187
186
|
const cachedUser2 = getSessionUserOverride();
|
|
188
187
|
if (cachedUser2) extraParams['_OverrideUserID'] = cachedUser2;
|
|
@@ -244,6 +243,8 @@ async function processPendingDeletes(client, options, modifyKey = null, transact
|
|
|
244
243
|
if (remaining.length > 0) {
|
|
245
244
|
log.warn(`${remaining.length} deletion(s) failed and remain staged.`);
|
|
246
245
|
}
|
|
246
|
+
|
|
247
|
+
return deletedUids.length;
|
|
247
248
|
}
|
|
248
249
|
|
|
249
250
|
/**
|
|
@@ -343,27 +344,12 @@ async function pushSingleFile(filePath, client, options, modifyKey = null, trans
|
|
|
343
344
|
try { meta = JSON.parse(await readFile(metaPath, 'utf8')); } catch {}
|
|
344
345
|
}
|
|
345
346
|
if (!meta) {
|
|
346
|
-
//
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
// After successful add, re-read the metadata (now has UID)
|
|
353
|
-
metaPath = binMeta.metaPath;
|
|
354
|
-
// The metadata file may have been renamed with ~UID, so scan for it
|
|
355
|
-
const updatedMeta = await findMetadataForCompanion(filePath);
|
|
356
|
-
if (updatedMeta) metaPath = updatedMeta;
|
|
357
|
-
meta = JSON.parse(await readFile(metaPath, 'utf8'));
|
|
358
|
-
log.info(`Successfully added — now pushing updates`);
|
|
359
|
-
} catch (err) {
|
|
360
|
-
log.error(`Auto-add failed for "${basename(filePath)}": ${err.message}`);
|
|
361
|
-
process.exit(1);
|
|
362
|
-
}
|
|
363
|
-
} else {
|
|
364
|
-
log.error(`No metadata found for "${basename(filePath)}". Pull the record first with "dbo pull".`);
|
|
365
|
-
process.exit(1);
|
|
366
|
-
}
|
|
347
|
+
// AUTO-ADD DISABLED: local-file-first approach is being replaced by server-first workflow.
|
|
348
|
+
// New records should be created via the DBO REST API first, then `dbo pull` to populate locally.
|
|
349
|
+
// The auto-add code (detectBinFile + submitAdd) has been commented out intentionally.
|
|
350
|
+
// See: .claude/2_specs/auto-deploy-config-generation.md — "server-first approach"
|
|
351
|
+
log.error(`No metadata found for "${basename(filePath)}". Create the record on the server first, then run "dbo pull".`);
|
|
352
|
+
process.exit(1);
|
|
367
353
|
}
|
|
368
354
|
}
|
|
369
355
|
|
|
@@ -473,61 +459,16 @@ async function ensureManifestMetadata() {
|
|
|
473
459
|
/**
|
|
474
460
|
* Push all records found in a directory (recursive)
|
|
475
461
|
*/
|
|
476
|
-
async function pushDirectory(dirPath, client, options, modifyKey = null, transactionKey = 'RowUID') {
|
|
462
|
+
async function pushDirectory(dirPath, client, options, modifyKey = null, transactionKey = 'RowUID', deletedCount = 0) {
|
|
477
463
|
// Auto-create manifest.metadata.json if manifest.json exists at root without companion metadata
|
|
478
464
|
await ensureManifestMetadata();
|
|
479
465
|
|
|
480
|
-
//
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
const unadded = await findUnaddedFiles(dirPath, ig);
|
|
485
|
-
if (unadded.length > 0) {
|
|
486
|
-
// Filter to files that detectBinFile can auto-classify (content/media in bins)
|
|
487
|
-
const autoAddable = [];
|
|
488
|
-
for (const filePath of unadded) {
|
|
489
|
-
const binMeta = await detectBinFile(filePath);
|
|
490
|
-
if (binMeta) autoAddable.push({ filePath, ...binMeta });
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
if (autoAddable.length > 0) {
|
|
494
|
-
log.info(`Found ${autoAddable.length} new file(s) to add before push:`);
|
|
495
|
-
for (const { filePath } of autoAddable) {
|
|
496
|
-
log.plain(` ${relative(process.cwd(), filePath)}`);
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
const doAdd = async () => {
|
|
500
|
-
for (const { meta, metaPath, filePath } of autoAddable) {
|
|
501
|
-
try {
|
|
502
|
-
await submitAdd(meta, metaPath, filePath, client, options);
|
|
503
|
-
// After submitAdd, meta.UID is set if successful
|
|
504
|
-
if (meta.UID) justAddedUIDs.add(meta.UID);
|
|
505
|
-
} catch (err) {
|
|
506
|
-
log.error(`Failed to add ${relative(process.cwd(), filePath)}: ${err.message}`);
|
|
507
|
-
}
|
|
508
|
-
}
|
|
509
|
-
};
|
|
510
|
-
|
|
511
|
-
if (!options.yes) {
|
|
512
|
-
const inquirer = (await import('inquirer')).default;
|
|
513
|
-
const { proceed } = await inquirer.prompt([{
|
|
514
|
-
type: 'confirm',
|
|
515
|
-
name: 'proceed',
|
|
516
|
-
message: `Add ${autoAddable.length} file(s) to the server before pushing?`,
|
|
517
|
-
default: true,
|
|
518
|
-
}]);
|
|
519
|
-
if (!proceed) {
|
|
520
|
-
log.dim('Skipping auto-add — continuing with push');
|
|
521
|
-
} else {
|
|
522
|
-
await doAdd();
|
|
523
|
-
}
|
|
524
|
-
} else {
|
|
525
|
-
await doAdd();
|
|
526
|
-
}
|
|
527
|
-
if (justAddedUIDs.size > 0) log.plain('');
|
|
528
|
-
}
|
|
529
|
-
}
|
|
466
|
+
// AUTO-ADD DISABLED: local-file-first approach is being replaced by server-first workflow.
|
|
467
|
+
// New records should be created via the DBO REST API first, then `dbo pull` to populate locally.
|
|
468
|
+
// The auto-add code (findUnaddedFiles + detectBinFile + submitAdd) has been commented out.
|
|
469
|
+
// See: .claude/2_specs/auto-deploy-config-generation.md — "server-first approach"
|
|
530
470
|
|
|
471
|
+
const ig = await loadIgnore();
|
|
531
472
|
const metaFiles = await findMetadataFiles(dirPath, ig);
|
|
532
473
|
|
|
533
474
|
// ── Load scripts config early (before delta detection) ──────────────
|
|
@@ -614,16 +555,16 @@ async function pushDirectory(dirPath, client, options, modifyKey = null, transac
|
|
|
614
555
|
continue;
|
|
615
556
|
}
|
|
616
557
|
|
|
617
|
-
//
|
|
618
|
-
if (meta.UID && justAddedUIDs.has(meta.UID)) {
|
|
619
|
-
log.dim(` Skipped (just added): ${basename(metaPath)}`);
|
|
620
|
-
continue;
|
|
621
|
-
}
|
|
558
|
+
// AUTO-ADD DISABLED: justAddedUIDs skip removed (server-first workflow)
|
|
622
559
|
|
|
623
|
-
//
|
|
624
|
-
//
|
|
625
|
-
|
|
626
|
-
|
|
560
|
+
// Output hierarchy entities: only push compound output files (root with inline children).
|
|
561
|
+
// Flat output metadata without .children are skipped — they are pushed as part of
|
|
562
|
+
// their parent compound file, or via `dbo deploy`. Never as standalone records.
|
|
563
|
+
if (meta._entity === 'output' || meta._entity === 'output_value'
|
|
564
|
+
|| meta._entity === 'output_value_filter' || meta._entity === 'output_value_entity_column_rel') {
|
|
565
|
+
if (meta._entity === 'output' && meta.children) {
|
|
566
|
+
outputCompoundFiles.push({ meta, metaPath });
|
|
567
|
+
}
|
|
627
568
|
continue;
|
|
628
569
|
}
|
|
629
570
|
|
|
@@ -719,23 +660,44 @@ async function pushDirectory(dirPath, client, options, modifyKey = null, transac
|
|
|
719
660
|
}
|
|
720
661
|
} catch { /* structure file missing or bin lookup failed — skip */ }
|
|
721
662
|
|
|
722
|
-
|
|
723
|
-
|
|
663
|
+
// Pre-filter compound output files: run delta detection early so unchanged outputs
|
|
664
|
+
// are excluded from the record count and ticket prompt (avoids false-positive prompts).
|
|
665
|
+
const outputsWithChanges = [];
|
|
666
|
+
for (const item of outputCompoundFiles) {
|
|
667
|
+
if (baseline) {
|
|
668
|
+
try {
|
|
669
|
+
const delta = await detectOutputChanges(item.metaPath, baseline);
|
|
670
|
+
const totalChanges = delta.root.length +
|
|
671
|
+
(delta.children ? Object.values(delta.children).reduce((s, c) => s + c.length, 0) : 999);
|
|
672
|
+
if (totalChanges === 0) {
|
|
673
|
+
log.dim(` Skipping ${basename(item.metaPath)} — no changes detected`);
|
|
674
|
+
skipped++;
|
|
675
|
+
continue;
|
|
676
|
+
}
|
|
677
|
+
} catch { /* delta detection failed — include in push for safety */ }
|
|
678
|
+
}
|
|
679
|
+
outputsWithChanges.push(item);
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
if (toPush.length === 0 && outputsWithChanges.length === 0 && binPushItems.length === 0) {
|
|
683
|
+
if (metaFiles.length === 0 && deletedCount === 0) {
|
|
724
684
|
log.warn(`No .metadata.json files found in "${dirPath}".`);
|
|
685
|
+
} else if (deletedCount > 0) {
|
|
686
|
+
log.info(`${deletedCount} deletion(s) processed. No other changes to push.`);
|
|
725
687
|
} else {
|
|
726
688
|
log.info('No changes to push');
|
|
727
689
|
}
|
|
728
690
|
return;
|
|
729
691
|
}
|
|
730
692
|
|
|
731
|
-
log.info(`Found ${
|
|
693
|
+
log.info(`Found ${toPush.length + outputsWithChanges.length + binPushItems.length} record(s) to push`);
|
|
732
694
|
|
|
733
695
|
// Pre-flight ticket validation (only if no --ticket flag)
|
|
734
|
-
const totalRecords = toPush.length +
|
|
696
|
+
const totalRecords = toPush.length + outputsWithChanges.length + binPushItems.length;
|
|
735
697
|
if (!options.ticket && totalRecords > 0) {
|
|
736
698
|
const recordSummary = [
|
|
737
699
|
...toPush.map(r => { const p = parseMetaFilename(basename(r.metaPath)); return p ? p.naturalBase : basename(r.metaPath, '.metadata.json'); }),
|
|
738
|
-
...
|
|
700
|
+
...outputsWithChanges.map(r => basename(r.metaPath, '.json')),
|
|
739
701
|
...binPushItems.map(r => `bin:${r.meta.Name}`),
|
|
740
702
|
].join(', ');
|
|
741
703
|
const ticketCheck = await checkStoredTicket(options, `${totalRecords} record(s): ${recordSummary}`);
|
|
@@ -835,8 +797,8 @@ async function pushDirectory(dirPath, client, options, modifyKey = null, transac
|
|
|
835
797
|
}
|
|
836
798
|
}
|
|
837
799
|
|
|
838
|
-
// Process compound output files (root + inline children)
|
|
839
|
-
for (const { meta, metaPath } of
|
|
800
|
+
// Process compound output files (root + inline children) — already pre-filtered for changes
|
|
801
|
+
for (const { meta, metaPath } of outputsWithChanges) {
|
|
840
802
|
try {
|
|
841
803
|
const result = await pushOutputCompound(meta, metaPath, client, options, baseline, modifyKey, transactionKey);
|
|
842
804
|
if (result.pushed > 0) {
|
|
@@ -878,11 +840,9 @@ async function pushDirectory(dirPath, client, options, modifyKey = null, transac
|
|
|
878
840
|
await updateBaselineAfterPush(baseline, successfulPushes);
|
|
879
841
|
}
|
|
880
842
|
|
|
881
|
-
// Re-tag successfully pushed files as Synced (best-effort)
|
|
882
|
-
for (const {
|
|
883
|
-
|
|
884
|
-
setFileTag(filePath, 'synced').catch(() => {});
|
|
885
|
-
}
|
|
843
|
+
// Re-tag successfully pushed metadata files as Synced (best-effort)
|
|
844
|
+
for (const { metaPath } of successfulPushes) {
|
|
845
|
+
setFileTag(metaPath, 'synced').catch(() => {});
|
|
886
846
|
}
|
|
887
847
|
|
|
888
848
|
log.info(`Push complete: ${succeeded} succeeded, ${failed} failed, ${skipped} skipped`);
|
package/src/lib/tagging.js
CHANGED
|
@@ -80,12 +80,15 @@ export async function tagProjectFiles(options = {}) {
|
|
|
80
80
|
const counts = { synced: 0, modified: 0, untracked: 0, conflict: 0, trashed: 0 };
|
|
81
81
|
|
|
82
82
|
if (clearAll) {
|
|
83
|
-
|
|
84
|
-
|
|
83
|
+
// Clear tags from ALL files in the project: metadata, companions, and any
|
|
84
|
+
// legacy-tagged content files (from old untracked detection).
|
|
85
|
+
const contentFiles = await _collectContentFiles(dir, ig);
|
|
86
|
+
const allFiles = [...metaPaths, ...contentFiles];
|
|
87
|
+
await _bulkApplyTags(allFiles.map(fp => ({ filePath: fp, clear: true })));
|
|
85
88
|
return null;
|
|
86
89
|
}
|
|
87
|
-
|
|
88
|
-
//
|
|
90
|
+
// Tag metadata files — the source of truth for sync status.
|
|
91
|
+
// Companion files are dependent artifacts and should not carry their own tags.
|
|
89
92
|
const toTag = [];
|
|
90
93
|
for (const metaPath of metaPaths) {
|
|
91
94
|
const inTrash = metaPath.replace(/\\/g, '/').includes('/trash/');
|
|
@@ -93,20 +96,9 @@ export async function tagProjectFiles(options = {}) {
|
|
|
93
96
|
? 'trashed'
|
|
94
97
|
: (await hasLocalModifications(metaPath, config).catch(() => false)) ? 'modified'
|
|
95
98
|
: 'synced';
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
counts[status]++;
|
|
100
|
-
if (verbose) console.log(` ${status.padEnd(10)} ${relative(dir, filePath)}`);
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
// Detect untracked files (content files without any metadata)
|
|
105
|
-
const untrackedFiles = await _findUntrackedFiles(dir, ig, metaPaths);
|
|
106
|
-
for (const filePath of untrackedFiles) {
|
|
107
|
-
toTag.push({ filePath, status: 'untracked' });
|
|
108
|
-
counts.untracked++;
|
|
109
|
-
if (verbose) console.log(` untracked ${relative(dir, filePath)}`);
|
|
99
|
+
toTag.push({ filePath: metaPath, status });
|
|
100
|
+
counts[status]++;
|
|
101
|
+
if (verbose) console.log(` ${status.padEnd(10)} ${relative(dir, metaPath)}`);
|
|
110
102
|
}
|
|
111
103
|
|
|
112
104
|
await _bulkApplyTags(toTag);
|
|
@@ -132,6 +124,11 @@ async function _getCompanionPaths(metaPath) {
|
|
|
132
124
|
try { await stat(candidate); paths.push(candidate); } catch { /* missing */ }
|
|
133
125
|
}
|
|
134
126
|
}
|
|
127
|
+
// Check _mediaFile (single media companion reference)
|
|
128
|
+
if (meta._mediaFile && String(meta._mediaFile).startsWith("@")) {
|
|
129
|
+
const candidate = join(dir, String(meta._mediaFile).substring(1));
|
|
130
|
+
try { await stat(candidate); paths.push(candidate); } catch { /* missing */ }
|
|
131
|
+
}
|
|
135
132
|
for (const col of (meta._mediaColumns || [])) {
|
|
136
133
|
const ref = meta[col];
|
|
137
134
|
if (ref && String(ref).startsWith('@')) {
|