@dboio/cli 0.16.2 → 0.17.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.
@@ -0,0 +1,534 @@
1
+ import { Command } from 'commander';
2
+ import { readFile, writeFile, stat, mkdir } from 'fs/promises';
3
+ import { join, dirname, basename, extname, relative } from 'path';
4
+ import { log } from '../lib/logger.js';
5
+ import { loadIgnore } from '../lib/ignore.js';
6
+ import { loadAppConfig, loadAppJsonBaseline, loadExtensionDocumentationMDPlacement, loadDescriptorFilenamePreference } from '../lib/config.js';
7
+ import {
8
+ resolveDirective, resolveTemplateCols, assembleMetadata,
9
+ promptReferenceColumn, getTemplateCols, setTemplateCols,
10
+ loadMetadataSchema, saveMetadataSchema,
11
+ } from '../lib/metadata-schema.js';
12
+ import { loadStructureFile, findBinByPath, BINS_DIR } from '../lib/structure.js';
13
+ import { isMetadataFile } from '../lib/filenames.js';
14
+ import { findUnaddedFiles, MIME_TYPES, seedMetadataTemplate, detectDocumentationFile, detectManifestFile } from '../lib/insert.js';
15
+ import { runPendingMigrations } from '../lib/migrations.js';
16
+
17
+ export const adoptCommand = new Command('adopt')
18
+ .description('Create metadata companion for a file or directory (local only — use dbo push to insert)')
19
+ .argument('<path>', 'File path, directory path, or "." for current directory')
20
+ .option('-e, --entity <spec>', 'Entity and optional column: e.g. content, media, extension.widget, extension.String5')
21
+ .option('--into <metaPath>', 'Attach file to an existing metadata record as an additional companion column (single-file only)')
22
+ .option('-y, --yes', 'Skip all confirmation prompts and use defaults')
23
+ .option('-v, --verbose', 'Show verbose output')
24
+ .option('--domain <host>', 'Override domain')
25
+ .option('--no-migrate', 'Skip pending migrations for this invocation')
26
+ .action(async (targetPath, options) => {
27
+ try {
28
+ await runPendingMigrations(options);
29
+
30
+ // --into is single-file only
31
+ if (options.into) {
32
+ if (targetPath === '.' || (await stat(targetPath)).isDirectory()) {
33
+ log.error('--into is not supported in directory mode');
34
+ process.exit(1);
35
+ }
36
+ await adoptIntoRecord(targetPath, options.into, options.entity, options);
37
+ return;
38
+ }
39
+
40
+ const pathStat = await stat(targetPath).catch(() => null);
41
+ if (!pathStat) {
42
+ log.error(`Path not found: ${targetPath}`);
43
+ process.exit(1);
44
+ }
45
+
46
+ if (pathStat.isDirectory()) {
47
+ await adoptDirectory(targetPath, options.entity, options);
48
+ } else {
49
+ await adoptSingleFile(targetPath, options.entity, options);
50
+ }
51
+ } catch (err) {
52
+ log.error(err.message);
53
+ process.exit(1);
54
+ }
55
+ });
56
+
57
+ /**
58
+ * Disambiguate extension.widget (descriptor) from extension.String5 (column).
59
+ */
60
+ function parseEntitySpec(spec, metadataSchema) {
61
+ if (!spec) return null;
62
+ const dotIdx = spec.indexOf('.');
63
+ if (dotIdx === -1) return { entity: spec, descriptor: null, column: null };
64
+
65
+ const entity = spec.slice(0, dotIdx);
66
+ const sub = spec.slice(dotIdx + 1);
67
+
68
+ if (entity === 'extension') {
69
+ // Check if `sub` is a known descriptor in metadata_schema.json
70
+ const extSchema = metadataSchema?.extension;
71
+ if (extSchema && typeof extSchema === 'object' && !Array.isArray(extSchema) && extSchema[sub] !== undefined) {
72
+ return { entity: 'extension', descriptor: sub, column: null };
73
+ }
74
+ // Otherwise treat as column name
75
+ return { entity: 'extension', descriptor: null, column: sub };
76
+ }
77
+
78
+ // e.g. "content.Content" — treat sub as explicit column
79
+ return { entity, descriptor: null, column: sub };
80
+ }
81
+
82
+ /**
83
+ * Build entity-specific metadata for a file in or outside lib/bins/.
84
+ */
85
+ async function buildBinMetadata(filePath, entity, appConfig, structure) {
86
+ const rel = relative(process.cwd(), filePath).replace(/\\/g, '/');
87
+ const ext = extname(filePath).replace('.', '').toLowerCase();
88
+ const fileName = basename(filePath);
89
+ const base = basename(filePath, extname(filePath));
90
+ const fileDir = dirname(rel);
91
+ const bin = findBinByPath(fileDir, structure);
92
+ const binPath = bin?.path || '';
93
+
94
+ const metaPath = join(dirname(filePath), `${base}.metadata.json`);
95
+
96
+ if (entity === 'content') {
97
+ const contentPath = binPath
98
+ ? `${binPath}/${base}.${ext}`
99
+ : `${base}.${ext}`;
100
+ const meta = {
101
+ _entity: 'content',
102
+ _companionReferenceColumns: ['Content'],
103
+ Name: base,
104
+ Content: `@${fileName}`,
105
+ Extension: ext.toUpperCase(),
106
+ Path: contentPath,
107
+ Active: 1,
108
+ Public: 0,
109
+ };
110
+ if (bin) meta.BinID = bin.binId;
111
+ if (appConfig.AppID) meta.AppID = appConfig.AppID;
112
+ return { meta, metaPath };
113
+ }
114
+
115
+ if (entity === 'media') {
116
+ const meta = {
117
+ _entity: 'media',
118
+ _mediaFile: `@${fileName}`,
119
+ Name: base,
120
+ Filename: fileName,
121
+ Extension: ext,
122
+ Ownership: 'App',
123
+ };
124
+ if (bin) meta.BinID = bin.binId;
125
+ if (appConfig.AppID) meta.AppID = appConfig.AppID;
126
+ if (binPath) meta.Path = binPath;
127
+ const mimeType = MIME_TYPES[ext];
128
+ if (mimeType) meta.MimeType = mimeType;
129
+ if (appConfig.AppShortName) {
130
+ const parts = ['', 'media', appConfig.AppShortName, 'app'];
131
+ if (binPath) parts.push(binPath);
132
+ parts.push(fileName);
133
+ meta.FullPath = parts.join('/');
134
+ }
135
+ return { meta, metaPath };
136
+ }
137
+
138
+ return null; // unsupported entity for bin metadata
139
+ }
140
+
141
+ /**
142
+ * Adopt a single file — create a *.metadata.json companion (local only, no server call).
143
+ */
144
+ async function adoptSingleFile(filePath, entityArg, options) {
145
+ const ig = await loadIgnore();
146
+ const relPath = relative(process.cwd(), filePath).replace(/\\/g, '/');
147
+ if (ig.ignores(relPath)) {
148
+ log.dim(`Skipped (dboignored): ${relPath}`);
149
+ return;
150
+ }
151
+
152
+ const dir = dirname(filePath);
153
+ const ext = extname(filePath);
154
+ const base = basename(filePath, ext);
155
+ const fileName = basename(filePath);
156
+
157
+ // Check for existing metadata
158
+ const metaPath = join(dir, `${base}.metadata.json`);
159
+ let existingMeta = null;
160
+ try {
161
+ existingMeta = JSON.parse(await readFile(metaPath, 'utf8'));
162
+ } catch { /* no file — that's fine */ }
163
+
164
+ if (existingMeta) {
165
+ if (existingMeta.UID || existingMeta._CreatedOn) {
166
+ log.warn(`"${fileName}" is already on the server (has UID/_CreatedOn) — skipping.`);
167
+ return;
168
+ }
169
+ // Metadata exists but no server record
170
+ if (!options.yes) {
171
+ const inquirer = (await import('inquirer')).default;
172
+ const { overwrite } = await inquirer.prompt([{
173
+ type: 'confirm',
174
+ name: 'overwrite',
175
+ message: `Metadata already exists for "${fileName}" (no UID). Overwrite?`,
176
+ default: false,
177
+ }]);
178
+ if (!overwrite) return;
179
+ }
180
+ }
181
+
182
+ const metadataSchema = await loadMetadataSchema() ?? {};
183
+
184
+ // --- Resolve entity from -e flag or directory inference ---
185
+ let entity, descriptor, column;
186
+ if (entityArg) {
187
+ const parsed = parseEntitySpec(entityArg, metadataSchema);
188
+ entity = parsed.entity;
189
+ descriptor = parsed.descriptor;
190
+ column = parsed.column;
191
+ } else {
192
+ // No -e: infer via resolveDirective
193
+ const directive = resolveDirective(filePath);
194
+ if (directive) {
195
+ entity = directive.entity;
196
+ descriptor = directive.descriptor;
197
+ } else {
198
+ // Fall back to interactive wizard
199
+ log.warn(`Cannot infer entity for "${fileName}". Use -e <entity> to specify.`);
200
+ await runInteractiveWizard(filePath, options);
201
+ return;
202
+ }
203
+ }
204
+
205
+ const appConfig = await loadAppConfig();
206
+
207
+ // --- Special case: manifest.json ---
208
+ const rel = relative(process.cwd(), filePath).replace(/\\/g, '/');
209
+ if (rel.toLowerCase() === 'manifest.json') {
210
+ const manifestResult = await detectManifestFile(filePath);
211
+ if (manifestResult) {
212
+ log.success(`Created manifest metadata at ${manifestResult.metaPath}`);
213
+ return;
214
+ }
215
+ }
216
+
217
+ // --- content or media entities: build entity-specific metadata ---
218
+ if ((entity === 'content' || entity === 'media') && !column) {
219
+ const structure = await loadStructureFile();
220
+ const result = await buildBinMetadata(filePath, entity, appConfig, structure);
221
+ if (result) {
222
+ await writeFile(result.metaPath, JSON.stringify(result.meta, null, 2) + '\n');
223
+ log.success(`Created ${entity} metadata: ${basename(result.metaPath)}`);
224
+ await seedMetadataTemplate();
225
+ return;
226
+ }
227
+ }
228
+
229
+ // --- docs/ files with documentation directive ---
230
+ if (entity === 'extension' && descriptor === 'documentation') {
231
+ const docBase = base;
232
+ const companionDir = join(process.cwd(), 'lib', 'extension', 'documentation');
233
+ await mkdir(companionDir, { recursive: true });
234
+ const docMetaPath = join(companionDir, `${docBase}.metadata.json`);
235
+
236
+ const docMeta = {
237
+ _entity: 'extension',
238
+ Descriptor: 'documentation',
239
+ Name: docBase,
240
+ };
241
+ const contentCol = 'String10';
242
+ docMeta[contentCol] = `@/${relPath}`;
243
+ docMeta._companionReferenceColumns = [contentCol];
244
+
245
+ await writeFile(docMetaPath, JSON.stringify(docMeta, null, 2) + '\n');
246
+ log.success(`Created documentation metadata: ${basename(docMetaPath)}`);
247
+ return;
248
+ }
249
+
250
+ // --- General path: use resolveTemplateCols + assembleMetadata ---
251
+ const appJson = await (async () => { try { return await loadAppJsonBaseline(); } catch { return null; } })();
252
+ const resolved = await resolveTemplateCols(entity, descriptor, appConfig, appJson);
253
+
254
+ if (resolved === null) {
255
+ // Malformed metadata_schema.json — fall through to wizard
256
+ await runInteractiveWizard(filePath, options);
257
+ return;
258
+ }
259
+
260
+ let { cols, templates } = resolved;
261
+
262
+ // If an explicit column was provided, inject it as @reference
263
+ if (column) {
264
+ const hasCol = cols.some(c => c.startsWith(column + '=') || c === column);
265
+ if (hasCol) {
266
+ cols = cols.map(c => (c === column || c.startsWith(column + '=')) ? `${column}=@reference` : c);
267
+ } else {
268
+ cols.push(`${column}=@reference`);
269
+ }
270
+ } else {
271
+ // Ensure @reference marker exists
272
+ const hasRefMarker = cols.some(c => c.includes('=@reference'));
273
+ if (!hasRefMarker) {
274
+ if (options.yes) {
275
+ log.warn(`No @reference column in template for ${entity}${descriptor ? '.' + descriptor : ''} — skipping content column.`);
276
+ } else {
277
+ const chosen = await promptReferenceColumn(cols, entity, descriptor);
278
+ if (chosen) {
279
+ cols = cols.map(c => c === chosen ? `${chosen}=@reference` : c);
280
+ templates = templates ?? metadataSchema;
281
+ setTemplateCols(templates, entity, descriptor, cols);
282
+ await saveMetadataSchema(templates);
283
+ }
284
+ }
285
+ }
286
+ }
287
+
288
+ const { meta } = assembleMetadata(cols, filePath, entity, descriptor, appConfig);
289
+ const finalMetaPath = join(dirname(filePath), `${base}.metadata.json`);
290
+
291
+ await writeFile(finalMetaPath, JSON.stringify(meta, null, 2) + '\n');
292
+ log.success(`Created metadata: ${basename(finalMetaPath)}`);
293
+ log.dim(` Run "dbo push" to insert this record on the server.`);
294
+ }
295
+
296
+ /**
297
+ * Interactive wizard fallback — creates metadata by prompting the user.
298
+ * Does NOT submit to server (local-only).
299
+ */
300
+ async function runInteractiveWizard(filePath, options) {
301
+ const inquirer = (await import('inquirer')).default;
302
+ const fileName = basename(filePath);
303
+ const base = basename(filePath, extname(filePath));
304
+ const metaPath = join(dirname(filePath), `${base}.metadata.json`);
305
+
306
+ log.plain('');
307
+ log.warn(`I cannot adopt "${fileName}" because I don't have enough metadata.`);
308
+ log.plain(`I have a few things that I need, and I can set that up for you,`);
309
+ log.plain(`or you can manually create "${base}.metadata.json" next to the file.`);
310
+ log.plain('');
311
+
312
+ if (!options.yes) {
313
+ const { proceed } = await inquirer.prompt([{
314
+ type: 'confirm',
315
+ name: 'proceed',
316
+ message: 'Want me to set that up for you?',
317
+ default: true,
318
+ }]);
319
+
320
+ if (!proceed) {
321
+ log.dim(`Skipping "${fileName}". Create "${base}.metadata.json" manually and try again.`);
322
+ return;
323
+ }
324
+ }
325
+
326
+ const appConfig = await loadAppConfig();
327
+
328
+ const answers = await inquirer.prompt([
329
+ {
330
+ type: 'input',
331
+ name: 'entity',
332
+ message: 'Entity name (e.g. content, template, style):',
333
+ default: 'content',
334
+ validate: v => v.trim() ? true : 'Entity is required',
335
+ },
336
+ {
337
+ type: 'input',
338
+ name: 'contentColumn',
339
+ message: 'Column name for the file content (e.g. Content, Body, Template):',
340
+ default: 'Content',
341
+ validate: v => v.trim() ? true : 'Column name is required',
342
+ },
343
+ {
344
+ type: 'input',
345
+ name: 'BinID',
346
+ message: 'BinID (optional, press Enter to skip):',
347
+ default: '',
348
+ },
349
+ {
350
+ type: 'input',
351
+ name: 'SiteID',
352
+ message: 'SiteID (optional, press Enter to skip):',
353
+ default: '',
354
+ },
355
+ {
356
+ type: 'input',
357
+ name: 'Path',
358
+ message: 'Path (optional, press Enter for auto-generated):',
359
+ default: relative(process.cwd(), filePath).replace(/\\/g, '/'),
360
+ },
361
+ ]);
362
+
363
+ // AppID: smart prompt when config has app info
364
+ if (appConfig.AppID) {
365
+ const { appIdChoice } = await inquirer.prompt([{
366
+ type: 'list',
367
+ name: 'appIdChoice',
368
+ message: `You're creating metadata without an AppID, but your config has information about the current App. Do you want me to add that Column information?`,
369
+ choices: [
370
+ { name: `Yes, use AppID ${appConfig.AppID}`, value: 'use_config' },
371
+ { name: 'No', value: 'none' },
372
+ { name: 'Enter custom AppID', value: 'custom' },
373
+ ],
374
+ }]);
375
+ if (appIdChoice === 'use_config') {
376
+ answers.AppID = String(appConfig.AppID);
377
+ } else if (appIdChoice === 'custom') {
378
+ const { customAppId } = await inquirer.prompt([{
379
+ type: 'input', name: 'customAppId',
380
+ message: 'Custom AppID:',
381
+ }]);
382
+ answers.AppID = customAppId;
383
+ } else {
384
+ answers.AppID = '';
385
+ }
386
+ } else {
387
+ const { appId } = await inquirer.prompt([{
388
+ type: 'input', name: 'appId',
389
+ message: 'AppID (optional, press Enter to skip):',
390
+ default: '',
391
+ }]);
392
+ answers.AppID = appId;
393
+ }
394
+
395
+ // Build metadata object
396
+ const meta = {};
397
+ if (answers.AppID.trim()) meta.AppID = isFinite(answers.AppID) ? Number(answers.AppID) : answers.AppID;
398
+ if (answers.BinID.trim()) meta.BinID = isFinite(answers.BinID) ? Number(answers.BinID) : answers.BinID;
399
+ if (answers.SiteID.trim()) meta.SiteID = isFinite(answers.SiteID) ? Number(answers.SiteID) : answers.SiteID;
400
+ meta.Name = base;
401
+ if (answers.Path.trim()) meta.Path = answers.Path.trim();
402
+ meta[answers.contentColumn.trim()] = `@${fileName}`;
403
+ meta._entity = answers.entity.trim();
404
+ meta._companionReferenceColumns = [answers.contentColumn.trim()];
405
+
406
+ // Write metadata file — NO server submission
407
+ await writeFile(metaPath, JSON.stringify(meta, null, 2) + '\n');
408
+ log.success(`Created ${basename(metaPath)}`);
409
+ log.dim(` Run "dbo push" to insert this record on the server.`);
410
+ }
411
+
412
+ /**
413
+ * Attach a file to an existing metadata record as an additional companion column.
414
+ */
415
+ async function adoptIntoRecord(filePath, intoPath, columnArg, options) {
416
+ if (!columnArg) {
417
+ log.error('-e <column> is required when using --into');
418
+ process.exit(1);
419
+ }
420
+
421
+ // Load target metadata
422
+ let targetMeta;
423
+ try {
424
+ targetMeta = JSON.parse(await readFile(intoPath, 'utf8'));
425
+ } catch {
426
+ log.error(`Target metadata not found: ${intoPath}`);
427
+ process.exit(1);
428
+ }
429
+
430
+ const targetEntity = targetMeta._entity;
431
+ if (!targetEntity) {
432
+ log.error(`Target metadata has no _entity field: ${intoPath}`);
433
+ process.exit(1);
434
+ }
435
+
436
+ // Parse column from -e (strip entity prefix if provided, validate if it doesn't match)
437
+ let column = columnArg;
438
+ const dotIdx = columnArg.indexOf('.');
439
+ if (dotIdx !== -1) {
440
+ const entityPrefix = columnArg.slice(0, dotIdx);
441
+ column = columnArg.slice(dotIdx + 1);
442
+ if (entityPrefix !== targetEntity) {
443
+ log.error(`Entity mismatch: -e specifies "${entityPrefix}" but target metadata has _entity "${targetEntity}"`);
444
+ process.exit(1);
445
+ }
446
+ }
447
+
448
+ // Validate column in metadata_schema.json
449
+ const metadataSchema = await loadMetadataSchema() ?? {};
450
+ const cols = getTemplateCols(metadataSchema, targetEntity, targetMeta.Descriptor ?? null);
451
+ if (cols && !cols.some(c => c === column || c.startsWith(column + '='))) {
452
+ log.warn(`Column "${column}" is not in the metadata schema for "${targetEntity}" — proceeding anyway.`);
453
+ }
454
+
455
+ // Check if column already set
456
+ if (targetMeta[column] !== undefined) {
457
+ if (!options.yes) {
458
+ const inquirer = (await import('inquirer')).default;
459
+ const { overwrite } = await inquirer.prompt([{
460
+ type: 'confirm',
461
+ name: 'overwrite',
462
+ message: `Column "${column}" already has value "${targetMeta[column]}" in ${basename(intoPath)}. Overwrite?`,
463
+ default: false,
464
+ }]);
465
+ if (!overwrite) return;
466
+ }
467
+ }
468
+
469
+ // Add column and update _companionReferenceColumns
470
+ const fileName = basename(filePath);
471
+ targetMeta[column] = `@${fileName}`;
472
+ const refs = targetMeta._companionReferenceColumns ?? [];
473
+ if (!refs.includes(column)) refs.push(column);
474
+ targetMeta._companionReferenceColumns = refs;
475
+
476
+ await writeFile(intoPath, JSON.stringify(targetMeta, null, 2) + '\n');
477
+ log.success(`Added ${column}: "@${fileName}" to ${basename(intoPath)}`);
478
+ log.dim(` _companionReferenceColumns: ${JSON.stringify(targetMeta._companionReferenceColumns)}`);
479
+ log.dim(` Run "dbo push" to insert/update the record on the server.`);
480
+ }
481
+
482
+ /**
483
+ * Adopt all un-tracked files in a directory.
484
+ */
485
+ async function adoptDirectory(dirPath, entityArg, options) {
486
+ if (!entityArg) {
487
+ log.error('The -e flag is required when adopting a directory.');
488
+ process.exit(1);
489
+ }
490
+
491
+ // Find files without metadata using the existing findUnaddedFiles logic
492
+ const ig = await loadIgnore();
493
+ const unadopted = await findUnaddedFiles(dirPath, ig);
494
+
495
+ if (unadopted.length === 0) {
496
+ log.info('Nothing to adopt — all files already have metadata.');
497
+ return;
498
+ }
499
+
500
+ const metadataSchema = await loadMetadataSchema() ?? {};
501
+ const parsed = parseEntitySpec(entityArg, metadataSchema);
502
+
503
+ log.info(`Found ${unadopted.length} file(s) to adopt as "${entityArg}":`);
504
+ for (const f of unadopted) {
505
+ log.plain(` ${relative(process.cwd(), f)}`);
506
+ }
507
+ log.plain('');
508
+
509
+ if (!options.yes) {
510
+ const inquirer = (await import('inquirer')).default;
511
+ const { proceed } = await inquirer.prompt([{
512
+ type: 'confirm',
513
+ name: 'proceed',
514
+ message: `Create metadata for ${unadopted.length} file(s) as "${entityArg}"?`,
515
+ default: true,
516
+ }]);
517
+ if (!proceed) return;
518
+ }
519
+
520
+ let succeeded = 0;
521
+ let skipped = 0;
522
+
523
+ for (const filePath of unadopted) {
524
+ try {
525
+ await adoptSingleFile(filePath, entityArg, { ...options, yes: true }); // suppress per-file prompts
526
+ succeeded++;
527
+ } catch (err) {
528
+ log.error(`Failed: ${relative(process.cwd(), filePath)} — ${err.message}`);
529
+ skipped++;
530
+ }
531
+ }
532
+
533
+ log.info(`Adopt complete: ${succeeded} created, ${skipped} failed/skipped.`);
534
+ }
@@ -1884,7 +1884,7 @@ async function processEntityDirEntries(entityName, entries, options, serverTz) {
1884
1884
  const configWithTz = { ...config, ServerTimezone: serverTz };
1885
1885
  const localSyncTime = await getLocalSyncTime(metaPath);
1886
1886
 
1887
- // If local metadata has no _LastUpdated (e.g. from dbo add), treat as server-newer
1887
+ // If local metadata has no _LastUpdated (e.g. from dbo adopt), treat as server-newer
1888
1888
  let localMissingLastUpdated = false;
1889
1889
  try {
1890
1890
  const localMeta = JSON.parse(await readFile(metaPath, 'utf8'));
@@ -1895,7 +1895,7 @@ async function processEntityDirEntries(entityName, entries, options, serverTz) {
1895
1895
  const serverDate = parseServerDate(record._LastUpdated, serverTz);
1896
1896
 
1897
1897
  if (serverNewer) {
1898
- // Incomplete metadata (no _LastUpdated) from dbo add — auto-accept without prompting
1898
+ // Incomplete metadata (no _LastUpdated) from dbo adopt — auto-accept without prompting
1899
1899
  if (localMissingLastUpdated) {
1900
1900
  log.dim(` Completing metadata: ${name}`);
1901
1901
  // Fall through to write
@@ -3133,7 +3133,7 @@ async function processRecord(entityName, record, structure, options, usedNames,
3133
3133
  const configWithTz = { ...config, ServerTimezone: serverTz };
3134
3134
  const localSyncTime = await getLocalSyncTime(metaPath);
3135
3135
 
3136
- // If local metadata has no _LastUpdated (e.g. from dbo add with incomplete fields),
3136
+ // If local metadata has no _LastUpdated (e.g. from dbo adopt with incomplete fields),
3137
3137
  // always treat as server-newer so pull populates missing columns.
3138
3138
  let localMissingLastUpdated = false;
3139
3139
  try {
@@ -3145,7 +3145,7 @@ async function processRecord(entityName, record, structure, options, usedNames,
3145
3145
  const serverDate = parseServerDate(record._LastUpdated, serverTz);
3146
3146
 
3147
3147
  if (serverNewer) {
3148
- // Incomplete metadata (no _LastUpdated) from dbo add — auto-accept without prompting
3148
+ // Incomplete metadata (no _LastUpdated) from dbo adopt — auto-accept without prompting
3149
3149
  if (localMissingLastUpdated) {
3150
3150
  log.dim(` Completing metadata: ${fileName}`);
3151
3151
  // Fall through to write
@@ -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
- // AUTO-ADD DISABLED: import { findUnaddedFiles, detectBinFile, submitAdd } from './add.js';
23
+ // AUTO-ADD DISABLED: import { findUnaddedFiles, detectBinFile, submitAdd } from '../lib/insert.js';
24
24
 
25
25
  /**
26
26
  * Resolve an @reference file path to an absolute filesystem path.
@@ -251,7 +251,7 @@ export async function renameToUidConvention(meta, metaPath, uid, lastUpdated, se
251
251
  }
252
252
 
253
253
  // Determine naturalBase from the temp/old metadata filename
254
- // Temp format from add.js: "colors.metadata.json" → naturalBase = "colors"
254
+ // Temp format from adopt.js: "colors.metadata.json" → naturalBase = "colors"
255
255
  // Old tilde format: "colors~uid.metadata.json" → naturalBase = "colors"
256
256
  let naturalBase;
257
257
  const legacyParsed = detectLegacyTildeMetadata(metaFilename);