@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dboio/cli",
3
- "version": "0.15.0",
3
+ "version": "0.15.2",
4
4
  "description": "CLI for the DBO.io framework",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dbo",
3
- "version": "0.8.0",
3
+ "version": "0.8.1",
4
4
  "description": "DBO.io CLI integration for Claude Code",
5
5
  "author": {
6
6
  "name": "DBO.io"
@@ -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) extraParams['_OverrideTicketID'] = 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
- // Try auto-detecting as a bin content/media file and add it first
347
- const binMeta = await detectBinFile(filePath);
348
- if (binMeta) {
349
- log.info(`No metadata foundauto-adding "${basename(filePath)}" first`);
350
- try {
351
- await submitAdd(binMeta.meta, binMeta.metaPath, filePath, client, options);
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
- // ── Auto-add: detect un-added files and create+submit them before push ──
481
- const ig = await loadIgnore();
482
- const justAddedUIDs = new Set(); // Track UIDs added in this invocation — skip from push loop
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
- // Skip records that were just auto-added in this invocation — they're already on the server
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
- // Compound output files: handle root + all inline children together
624
- // These have _entity='output' and inline children under .children
625
- if (meta._entity === 'output' && meta.children) {
626
- outputCompoundFiles.push({ meta, metaPath });
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
- if (toPush.length === 0 && outputCompoundFiles.length === 0 && binPushItems.length === 0) {
723
- if (metaFiles.length === 0) {
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 ${metaFiles.length} record(s) to push`);
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 + outputCompoundFiles.length + binPushItems.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
- ...outputCompoundFiles.map(r => basename(r.metaPath, '.json')),
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 outputCompoundFiles) {
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 { meta, metaPath } of successfulPushes) {
883
- for (const filePath of _getMetaCompanionPaths(meta, metaPath)) {
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`);
@@ -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
- const companions = (await Promise.all(metaPaths.map(mp => _getCompanionPaths(mp)))).flat();
84
- await _bulkApplyTags(companions.map(fp => ({ filePath: fp, clear: true })));
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
- // Collect file→status pairs from metadata
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
- const companions = await _getCompanionPaths(metaPath);
97
- for (const filePath of companions) {
98
- toTag.push({ filePath, status });
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('@')) {