@dboio/cli 0.15.0 → 0.15.1

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.1",
4
4
  "description": "CLI for the DBO.io framework",
5
5
  "type": "module",
6
6
  "bin": {
@@ -20,7 +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';
23
+ // AUTO-ADD DISABLED: import { findUnaddedFiles, detectBinFile, submitAdd } from './add.js';
24
24
 
25
25
  function _getMetaCompanionPaths(meta, metaPath) {
26
26
  const dir = dirname(metaPath);
@@ -96,7 +96,7 @@ export const pushCommand = new Command('push')
96
96
  const transactionKey = await resolveTransactionKey(options);
97
97
 
98
98
  // Process pending deletions from synchronize.json
99
- await processPendingDeletes(client, options, modifyKey, transactionKey);
99
+ const deletedCount = await processPendingDeletes(client, options, modifyKey, transactionKey) || 0;
100
100
 
101
101
  // ── Resolution order ──────────────────────────────────────────
102
102
  // 1. Commas → UID list
@@ -135,7 +135,7 @@ export const pushCommand = new Command('push')
135
135
  log.dim(` Found: ${relative(process.cwd(), resolved)}`);
136
136
  const resolvedStat = await stat(resolved);
137
137
  if (resolvedStat.isDirectory()) {
138
- await pushDirectory(resolved, client, options, modifyKey, transactionKey);
138
+ await pushDirectory(resolved, client, options, modifyKey, transactionKey, deletedCount);
139
139
  } else {
140
140
  await pushSingleFile(resolved, client, options, modifyKey, transactionKey);
141
141
  }
@@ -156,7 +156,7 @@ export const pushCommand = new Command('push')
156
156
  }
157
157
 
158
158
  if (pathStat.isDirectory()) {
159
- await pushDirectory(targetPath, client, options, modifyKey, transactionKey);
159
+ await pushDirectory(targetPath, client, options, modifyKey, transactionKey, deletedCount);
160
160
  } else {
161
161
  await pushSingleFile(targetPath, client, options, modifyKey, transactionKey);
162
162
  }
@@ -171,18 +171,27 @@ export const pushCommand = new Command('push')
171
171
  */
172
172
  async function processPendingDeletes(client, options, modifyKey = null, transactionKey = 'RowUID') {
173
173
  const sync = await loadSynchronize();
174
- if (!sync.delete || sync.delete.length === 0) return;
174
+ if (!sync.delete || sync.delete.length === 0) return 0;
175
175
 
176
176
  log.info(`Processing ${sync.delete.length} pending deletion(s)...`);
177
177
 
178
178
  const remaining = [];
179
179
  const deletedUids = [];
180
180
 
181
+ // Load stored ticket once for all deletions (same as main push loop)
182
+ const { getGlobalTicket, getRecordTicket } = await import('../lib/ticketing.js');
183
+ const globalTicket = !options.ticket ? await getGlobalTicket() : null;
184
+
181
185
  for (const entry of sync.delete) {
182
186
  log.info(`Deleting "${entry.name}" (${entry.entity}:${entry.RowID})`);
183
187
 
184
188
  const extraParams = { '_confirm': options.confirm || 'true' };
185
- if (options.ticket) extraParams['_OverrideTicketID'] = options.ticket;
189
+ if (options.ticket) {
190
+ extraParams['_OverrideTicketID'] = options.ticket;
191
+ } else {
192
+ const ticket = await getRecordTicket(entry.UID) || globalTicket;
193
+ if (ticket) extraParams['_OverrideTicketID'] = ticket;
194
+ }
186
195
  if (modifyKey) extraParams['_modify_key'] = modifyKey;
187
196
  const cachedUser2 = getSessionUserOverride();
188
197
  if (cachedUser2) extraParams['_OverrideUserID'] = cachedUser2;
@@ -244,6 +253,8 @@ async function processPendingDeletes(client, options, modifyKey = null, transact
244
253
  if (remaining.length > 0) {
245
254
  log.warn(`${remaining.length} deletion(s) failed and remain staged.`);
246
255
  }
256
+
257
+ return deletedUids.length;
247
258
  }
248
259
 
249
260
  /**
@@ -343,27 +354,12 @@ async function pushSingleFile(filePath, client, options, modifyKey = null, trans
343
354
  try { meta = JSON.parse(await readFile(metaPath, 'utf8')); } catch {}
344
355
  }
345
356
  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
- }
357
+ // AUTO-ADD DISABLED: local-file-first approach is being replaced by server-first workflow.
358
+ // New records should be created via the DBO REST API first, then `dbo pull` to populate locally.
359
+ // The auto-add code (detectBinFile + submitAdd) has been commented out intentionally.
360
+ // See: .claude/2_specs/auto-deploy-config-generation.md"server-first approach"
361
+ log.error(`No metadata found for "${basename(filePath)}". Create the record on the server first, then run "dbo pull".`);
362
+ process.exit(1);
367
363
  }
368
364
  }
369
365
 
@@ -473,61 +469,16 @@ async function ensureManifestMetadata() {
473
469
  /**
474
470
  * Push all records found in a directory (recursive)
475
471
  */
476
- async function pushDirectory(dirPath, client, options, modifyKey = null, transactionKey = 'RowUID') {
472
+ async function pushDirectory(dirPath, client, options, modifyKey = null, transactionKey = 'RowUID', deletedCount = 0) {
477
473
  // Auto-create manifest.metadata.json if manifest.json exists at root without companion metadata
478
474
  await ensureManifestMetadata();
479
475
 
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
- }
476
+ // AUTO-ADD DISABLED: local-file-first approach is being replaced by server-first workflow.
477
+ // New records should be created via the DBO REST API first, then `dbo pull` to populate locally.
478
+ // The auto-add code (findUnaddedFiles + detectBinFile + submitAdd) has been commented out.
479
+ // See: .claude/2_specs/auto-deploy-config-generation.md — "server-first approach"
530
480
 
481
+ const ig = await loadIgnore();
531
482
  const metaFiles = await findMetadataFiles(dirPath, ig);
532
483
 
533
484
  // ── Load scripts config early (before delta detection) ──────────────
@@ -614,16 +565,16 @@ async function pushDirectory(dirPath, client, options, modifyKey = null, transac
614
565
  continue;
615
566
  }
616
567
 
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
- }
568
+ // AUTO-ADD DISABLED: justAddedUIDs skip removed (server-first workflow)
622
569
 
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 });
570
+ // Output hierarchy entities: only push compound output files (root with inline children).
571
+ // Flat output metadata without .children are skipped — they are pushed as part of
572
+ // their parent compound file, or via `dbo deploy`. Never as standalone records.
573
+ if (meta._entity === 'output' || meta._entity === 'output_value'
574
+ || meta._entity === 'output_value_filter' || meta._entity === 'output_value_entity_column_rel') {
575
+ if (meta._entity === 'output' && meta.children) {
576
+ outputCompoundFiles.push({ meta, metaPath });
577
+ }
627
578
  continue;
628
579
  }
629
580
 
@@ -719,23 +670,44 @@ async function pushDirectory(dirPath, client, options, modifyKey = null, transac
719
670
  }
720
671
  } catch { /* structure file missing or bin lookup failed — skip */ }
721
672
 
722
- if (toPush.length === 0 && outputCompoundFiles.length === 0 && binPushItems.length === 0) {
723
- if (metaFiles.length === 0) {
673
+ // Pre-filter compound output files: run delta detection early so unchanged outputs
674
+ // are excluded from the record count and ticket prompt (avoids false-positive prompts).
675
+ const outputsWithChanges = [];
676
+ for (const item of outputCompoundFiles) {
677
+ if (baseline) {
678
+ try {
679
+ const delta = await detectOutputChanges(item.metaPath, baseline);
680
+ const totalChanges = delta.root.length +
681
+ (delta.children ? Object.values(delta.children).reduce((s, c) => s + c.length, 0) : 999);
682
+ if (totalChanges === 0) {
683
+ log.dim(` Skipping ${basename(item.metaPath)} — no changes detected`);
684
+ skipped++;
685
+ continue;
686
+ }
687
+ } catch { /* delta detection failed — include in push for safety */ }
688
+ }
689
+ outputsWithChanges.push(item);
690
+ }
691
+
692
+ if (toPush.length === 0 && outputsWithChanges.length === 0 && binPushItems.length === 0) {
693
+ if (metaFiles.length === 0 && deletedCount === 0) {
724
694
  log.warn(`No .metadata.json files found in "${dirPath}".`);
695
+ } else if (deletedCount > 0) {
696
+ log.info(`${deletedCount} deletion(s) processed. No other changes to push.`);
725
697
  } else {
726
698
  log.info('No changes to push');
727
699
  }
728
700
  return;
729
701
  }
730
702
 
731
- log.info(`Found ${metaFiles.length} record(s) to push`);
703
+ log.info(`Found ${toPush.length + outputsWithChanges.length + binPushItems.length} record(s) to push`);
732
704
 
733
705
  // Pre-flight ticket validation (only if no --ticket flag)
734
- const totalRecords = toPush.length + outputCompoundFiles.length + binPushItems.length;
706
+ const totalRecords = toPush.length + outputsWithChanges.length + binPushItems.length;
735
707
  if (!options.ticket && totalRecords > 0) {
736
708
  const recordSummary = [
737
709
  ...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')),
710
+ ...outputsWithChanges.map(r => basename(r.metaPath, '.json')),
739
711
  ...binPushItems.map(r => `bin:${r.meta.Name}`),
740
712
  ].join(', ');
741
713
  const ticketCheck = await checkStoredTicket(options, `${totalRecords} record(s): ${recordSummary}`);
@@ -835,8 +807,8 @@ async function pushDirectory(dirPath, client, options, modifyKey = null, transac
835
807
  }
836
808
  }
837
809
 
838
- // Process compound output files (root + inline children)
839
- for (const { meta, metaPath } of outputCompoundFiles) {
810
+ // Process compound output files (root + inline children) — already pre-filtered for changes
811
+ for (const { meta, metaPath } of outputsWithChanges) {
840
812
  try {
841
813
  const result = await pushOutputCompound(meta, metaPath, client, options, baseline, modifyKey, transactionKey);
842
814
  if (result.pushed > 0) {