@dboio/cli 0.16.2 → 0.19.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.
Files changed (45) hide show
  1. package/README.md +175 -138
  2. package/bin/dbo.js +2 -2
  3. package/package.json +1 -1
  4. package/plugins/claude/dbo/docs/dbo-cli-readme.md +175 -138
  5. package/src/commands/adopt.js +534 -0
  6. package/src/commands/build.js +3 -3
  7. package/src/commands/clone.js +209 -75
  8. package/src/commands/deploy.js +3 -3
  9. package/src/commands/init.js +11 -11
  10. package/src/commands/install.js +3 -3
  11. package/src/commands/login.js +2 -2
  12. package/src/commands/mv.js +15 -15
  13. package/src/commands/pull.js +1 -1
  14. package/src/commands/push.js +194 -15
  15. package/src/commands/rm.js +2 -2
  16. package/src/commands/run.js +4 -4
  17. package/src/commands/status.js +1 -1
  18. package/src/commands/sync.js +2 -2
  19. package/src/lib/config.js +186 -135
  20. package/src/lib/delta.js +119 -17
  21. package/src/lib/dependencies.js +51 -24
  22. package/src/lib/deploy-config.js +4 -4
  23. package/src/lib/domain-guard.js +8 -9
  24. package/src/lib/filenames.js +13 -2
  25. package/src/lib/ignore.js +2 -3
  26. package/src/{commands/add.js → lib/insert.js} +127 -472
  27. package/src/lib/metadata-schema.js +14 -20
  28. package/src/lib/metadata-templates.js +4 -4
  29. package/src/lib/migrations.js +1 -1
  30. package/src/lib/modify-key.js +1 -1
  31. package/src/lib/scaffold.js +5 -12
  32. package/src/lib/schema.js +67 -37
  33. package/src/lib/structure.js +6 -6
  34. package/src/lib/tagging.js +2 -2
  35. package/src/lib/ticketing.js +3 -7
  36. package/src/lib/toe-stepping.js +5 -5
  37. package/src/lib/transaction-key.js +1 -1
  38. package/src/migrations/004-rename-output-files.js +2 -2
  39. package/src/migrations/005-rename-output-metadata.js +2 -2
  40. package/src/migrations/006-remove-uid-companion-filenames.js +1 -1
  41. package/src/migrations/007-natural-entity-companion-filenames.js +1 -1
  42. package/src/migrations/008-metadata-uid-in-suffix.js +1 -1
  43. package/src/migrations/009-fix-media-collision-metadata-names.js +1 -1
  44. package/src/migrations/010-delete-paren-media-orphans.js +1 -1
  45. package/src/migrations/012-project-dir-restructure.js +211 -0
@@ -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
+ }
@@ -16,9 +16,9 @@ export const buildCommand = new Command('build')
16
16
 
17
17
  if (!base && !local) {
18
18
  if (targetPath) {
19
- log.warn('No .dbo/scripts.json found — nothing to build');
19
+ log.warn('No .app/scripts.json found — nothing to build');
20
20
  } else {
21
- log.info('No .dbo/scripts.json found');
21
+ log.info('No .app/scripts.json found');
22
22
  }
23
23
  return;
24
24
  }
@@ -63,7 +63,7 @@ export const buildCommand = new Command('build')
63
63
  if (targets.length === 0) return; // only global, done
64
64
  }
65
65
  if (targets.length === 0) {
66
- log.info('No build hooks defined in .dbo/scripts.json targets');
66
+ log.info('No build hooks defined in .app/scripts.json targets');
67
67
  return;
68
68
  }
69
69
  }