@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.
- package/README.md +65 -54
- package/bin/dbo.js +2 -2
- package/package.json +1 -1
- package/plugins/claude/dbo/docs/dbo-cli-readme.md +65 -54
- package/src/commands/adopt.js +534 -0
- package/src/commands/clone.js +4 -4
- package/src/commands/push.js +1 -1
- package/src/lib/filenames.js +1 -1
- package/src/{commands/add.js → lib/insert.js} +127 -472
|
@@ -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
|
+
}
|
package/src/commands/clone.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
package/src/commands/push.js
CHANGED
|
@@ -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 '
|
|
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.
|
package/src/lib/filenames.js
CHANGED
|
@@ -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
|
|
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);
|