@dboio/cli 0.17.0 → 0.19.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.
Files changed (43) hide show
  1. package/README.md +111 -85
  2. package/package.json +1 -1
  3. package/plugins/claude/dbo/docs/dbo-cli-readme.md +111 -85
  4. package/src/commands/build.js +3 -3
  5. package/src/commands/clone.js +236 -97
  6. package/src/commands/deploy.js +3 -3
  7. package/src/commands/init.js +11 -11
  8. package/src/commands/install.js +3 -3
  9. package/src/commands/login.js +2 -2
  10. package/src/commands/mv.js +15 -15
  11. package/src/commands/pull.js +1 -1
  12. package/src/commands/push.js +193 -14
  13. package/src/commands/rm.js +2 -2
  14. package/src/commands/run.js +4 -4
  15. package/src/commands/status.js +1 -1
  16. package/src/commands/sync.js +2 -2
  17. package/src/lib/config.js +186 -135
  18. package/src/lib/delta.js +119 -17
  19. package/src/lib/dependencies.js +51 -24
  20. package/src/lib/deploy-config.js +4 -4
  21. package/src/lib/domain-guard.js +8 -9
  22. package/src/lib/filenames.js +12 -1
  23. package/src/lib/ignore.js +2 -3
  24. package/src/lib/insert.js +1 -1
  25. package/src/lib/metadata-schema.js +14 -20
  26. package/src/lib/metadata-templates.js +4 -4
  27. package/src/lib/migrations.js +1 -1
  28. package/src/lib/modify-key.js +1 -1
  29. package/src/lib/scaffold.js +5 -12
  30. package/src/lib/schema.js +67 -37
  31. package/src/lib/structure.js +6 -6
  32. package/src/lib/tagging.js +2 -2
  33. package/src/lib/ticketing.js +3 -7
  34. package/src/lib/toe-stepping.js +5 -5
  35. package/src/lib/transaction-key.js +1 -1
  36. package/src/migrations/004-rename-output-files.js +2 -2
  37. package/src/migrations/005-rename-output-metadata.js +2 -2
  38. package/src/migrations/006-remove-uid-companion-filenames.js +1 -1
  39. package/src/migrations/007-natural-entity-companion-filenames.js +1 -1
  40. package/src/migrations/008-metadata-uid-in-suffix.js +1 -1
  41. package/src/migrations/009-fix-media-collision-metadata-names.js +1 -1
  42. package/src/migrations/010-delete-paren-media-orphans.js +1 -1
  43. package/src/migrations/012-project-dir-restructure.js +211 -0
package/src/lib/delta.js CHANGED
@@ -45,19 +45,16 @@ export function findBaselineEntry(baseline, entity, uid) {
45
45
  if (found) return found;
46
46
  }
47
47
 
48
- // For output hierarchy entities (output_value, output_value_filter,
49
- // output_value_entity_column_rel), also search nested inside each
50
- // output record's .children — server nests them per-output.
51
- if (entity !== 'output') {
52
- const outputs = baseline.children.output;
53
- if (Array.isArray(outputs)) {
54
- for (const o of outputs) {
55
- if (!o.children) continue;
56
- const nested = o.children[entity];
57
- if (Array.isArray(nested)) {
58
- const found = nested.find(item => item.UID === uid);
59
- if (found) return found;
60
- }
48
+ // Search nested inside parent records' .children — server nests children
49
+ // per-parent (e.g. output→output_value, entity→entity_column).
50
+ for (const [parentEntity, parentArray] of Object.entries(baseline.children)) {
51
+ if (!Array.isArray(parentArray)) continue;
52
+ for (const parent of parentArray) {
53
+ if (!parent.children) continue;
54
+ const nested = parent.children[entity];
55
+ if (Array.isArray(nested)) {
56
+ const found = nested.find(item => item.UID === uid);
57
+ if (found) return found;
61
58
  }
62
59
  }
63
60
  }
@@ -274,7 +271,7 @@ export function normalizeValue(value) {
274
271
 
275
272
  // ─── Compound Output Delta Detection ────────────────────────────────────────
276
273
 
277
- const _OUTPUT_DOC_KEYS = ['column', 'join', 'filter'];
274
+ const _OUTPUT_CHILD_KEYS = ['output_value', 'output_value_filter', 'output_value_entity_column_rel'];
278
275
 
279
276
  /**
280
277
  * Detect changed columns across a compound output file (root + inline children).
@@ -344,10 +341,10 @@ const BIN_TRACKED_COLUMNS = ['Name', 'Path', 'ParentBinID', 'Active', 'Public'];
344
341
 
345
342
  /**
346
343
  * Detect changes between a current bin entry (from structure.json) and the
347
- * baseline (from app.json children.bin array).
344
+ * baseline (from baseline children.bin array).
348
345
  *
349
346
  * @param {Object} binEntry - Current bin entry from structure.json (with binId, name, path, etc.)
350
- * @param {Object} baseline - The baseline JSON (app.json)
347
+ * @param {Object} baseline - The baseline JSON
351
348
  * @returns {string[]} - Array of changed column names
352
349
  */
353
350
  export function detectBinChanges(binEntry, baseline) {
@@ -411,7 +408,7 @@ export function synthesizeBinMetadata(binEntry, appId) {
411
408
  }
412
409
 
413
410
  async function _walkChildrenForChanges(childrenObj, baseline, metaDir, result) {
414
- for (const docKey of _OUTPUT_DOC_KEYS) {
411
+ for (const docKey of _OUTPUT_CHILD_KEYS) {
415
412
  const entityArray = childrenObj[docKey];
416
413
  if (!Array.isArray(entityArray) || entityArray.length === 0) continue;
417
414
  for (const child of entityArray) {
@@ -424,3 +421,108 @@ async function _walkChildrenForChanges(childrenObj, baseline, metaDir, result) {
424
421
  }
425
422
  }
426
423
  }
424
+
425
+ // ─── Generic Entity Children Delta Detection ─────────────────────────────────
426
+
427
+ /**
428
+ * Detect changed, added, and removed child records in a metadata file with `children`.
429
+ * Applies to entity dir metadata (e.g. entity→entity_column, extension→...).
430
+ * Does NOT apply to output hierarchy — use detectOutputChanges() for those.
431
+ *
432
+ * @param {string} metaPath - Path to the parent metadata file
433
+ * @param {Object} baseline - Loaded baseline JSON
434
+ * @returns {Promise<Object>} - { [childUID]: { childEntity, changedColumns, isNew, isRemoved } }
435
+ */
436
+ export async function detectEntityChildrenChanges(metaPath, baseline) {
437
+ const meta = JSON.parse(await readFile(metaPath, 'utf8'));
438
+ const { children, UID: parentUid, _entity: parentEntity } = meta;
439
+
440
+ if (!children || typeof children !== 'object') return {};
441
+
442
+ const result = {};
443
+
444
+ for (const [childEntityName, childArray] of Object.entries(children)) {
445
+ if (!Array.isArray(childArray)) continue;
446
+
447
+ // Collect UIDs present in current metadata
448
+ const metaChildUIDs = new Set(childArray.map(c => c.UID).filter(Boolean));
449
+
450
+ // Find baseline children for this parent + child entity combo
451
+ const baselineChildren = findChildrenInBaseline(baseline, parentEntity, parentUid, childEntityName);
452
+ const baselineByUID = new Map(baselineChildren.map(c => [c.UID, c]));
453
+
454
+ // Detect new and edited children
455
+ for (const child of childArray) {
456
+ const uid = child.UID;
457
+ if (!uid) continue;
458
+ const baselineChild = baselineByUID.get(uid);
459
+ if (!baselineChild) {
460
+ result[uid] = { childEntity: childEntityName, changedColumns: getAllUserColumns(child), isNew: true };
461
+ } else {
462
+ const changed = _compareChildToBaseline(child, baselineChild);
463
+ if (changed.length > 0) {
464
+ result[uid] = { childEntity: childEntityName, changedColumns: changed, isNew: false };
465
+ }
466
+ }
467
+ }
468
+
469
+ // Detect removed children (in baseline but not in metadata)
470
+ for (const [uid] of baselineByUID.entries()) {
471
+ if (!metaChildUIDs.has(uid)) {
472
+ result[uid] = { childEntity: childEntityName, changedColumns: [], isRemoved: true };
473
+ }
474
+ }
475
+ }
476
+
477
+ return result;
478
+ }
479
+
480
+ /**
481
+ * Find children in the baseline for a given parent record + child entity name.
482
+ * Checks the parent's nested children first; falls back to top-level baseline array.
483
+ *
484
+ * @param {Object} baseline - Loaded baseline JSON
485
+ * @param {string} parentEntity - e.g. 'entity'
486
+ * @param {string} parentUid - UID of the parent record
487
+ * @param {string} childEntityName - e.g. 'entity_column'
488
+ * @returns {Array} - Array of baseline child records
489
+ */
490
+ export function findChildrenInBaseline(baseline, parentEntity, parentUid, childEntityName) {
491
+ if (!baseline || !baseline.children) return [];
492
+
493
+ // First: try nested inside the parent entity record
494
+ const parentArray = baseline.children[parentEntity];
495
+ if (Array.isArray(parentArray)) {
496
+ const parentRecord = parentArray.find(p => p.UID === parentUid);
497
+ if (parentRecord?.children && Array.isArray(parentRecord.children[childEntityName])) {
498
+ return parentRecord.children[childEntityName];
499
+ }
500
+ }
501
+
502
+ // Fallback: top-level baseline array for the child entity (less common)
503
+ const topLevel = baseline.children[childEntityName];
504
+ return Array.isArray(topLevel) ? topLevel : [];
505
+ }
506
+
507
+ /**
508
+ * Compare a child record's fields against its baseline entry.
509
+ * Returns names of changed columns (excludes system columns and 'children').
510
+ */
511
+ function _compareChildToBaseline(child, baselineChild) {
512
+ const changed = [];
513
+ for (const [col, val] of Object.entries(child)) {
514
+ if (shouldSkipColumn(col) || col === 'children') continue;
515
+ if (normalizeValue(val) !== normalizeValue(baselineChild[col])) {
516
+ changed.push(col);
517
+ }
518
+ }
519
+ // Columns removed from metadata but present in baseline
520
+ for (const [col, baselineVal] of Object.entries(baselineChild)) {
521
+ if (shouldSkipColumn(col) || col === 'children') continue;
522
+ if (col in child) continue;
523
+ if (baselineVal !== null && baselineVal !== undefined && normalizeValue(baselineVal) !== '') {
524
+ changed.push(col);
525
+ }
526
+ }
527
+ return changed;
528
+ }
@@ -1,11 +1,11 @@
1
1
  /**
2
2
  * Dependency management for entity synchronization and app dependency cloning.
3
3
  * - Entity ordering: ensures children are processed before parents for referential integrity.
4
- * - App dependencies: auto-clone related apps into .dbo/dependencies/<shortname>/.
4
+ * - App dependencies: auto-clone related apps into app_dependencies/<shortname>/.
5
5
  */
6
6
 
7
7
  import { spawn } from 'child_process';
8
- import { mkdir, symlink, access, readFile, writeFile } from 'fs/promises';
8
+ import { mkdir, symlink, access, readFile, readdir, writeFile, rm } from 'fs/promises';
9
9
  import { join, resolve, relative, sep } from 'path';
10
10
  import { fileURLToPath } from 'url';
11
11
  import { DboClient } from './client.js';
@@ -219,7 +219,7 @@ export async function checkDependencyStaleness(shortname, options = {}) {
219
219
 
220
220
  const dateStr = stored.substring(0, 10); // YYYY-MM-DD from ISO string
221
221
  const result = await client.get(
222
- `/api/app/object/${encodeURIComponent(shortname)}[_LastUpdated]?UpdatedAfter=${dateStr}`
222
+ `/api/app/object/${encodeURIComponent(shortname)}?UpdatedAfter=${dateStr}`
223
223
  );
224
224
  if (!result.ok || !result.data) return false; // Can't determine — assume fresh
225
225
 
@@ -229,7 +229,7 @@ export async function checkDependencyStaleness(shortname, options = {}) {
229
229
  }
230
230
 
231
231
  /**
232
- * Sync dependency apps into .dbo/dependencies/<shortname>/.
232
+ * Sync dependency apps into app_dependencies/<shortname>/.
233
233
  *
234
234
  * @param {object} options
235
235
  * @param {string} [options.domain] - Override domain
@@ -243,7 +243,7 @@ export async function checkDependencyStaleness(shortname, options = {}) {
243
243
  export async function syncDependencies(options = {}) {
244
244
  // Recursive guard: don't run inside a checkout directory
245
245
  const cwd = process.cwd();
246
- if (cwd.includes(`${sep}.dbo${sep}dependencies${sep}`)) {
246
+ if (cwd.includes(`${sep}app_dependencies${sep}`)) {
247
247
  log.dim(' Skipping dependency sync (inside a checkout directory)');
248
248
  return;
249
249
  }
@@ -252,8 +252,8 @@ export async function syncDependencies(options = {}) {
252
252
  ? [...new Set(['_system', ...options.only])]
253
253
  : await getDependencies();
254
254
 
255
- const parentDboDir = join(cwd, '.dbo');
256
- const depsRoot = join(parentDboDir, 'dependencies');
255
+ const parentProjectDir = join(cwd, '.app');
256
+ const depsRoot = join(cwd, 'app_dependencies');
257
257
 
258
258
  const forceAll = !!(options.force || options.schema);
259
259
  const execFn = options._execOverride || execDboInDir;
@@ -271,12 +271,12 @@ export async function syncDependencies(options = {}) {
271
271
  spinner.update(`Syncing dependency: ${shortname}`);
272
272
 
273
273
  const checkoutDir = join(depsRoot, shortname);
274
- const checkoutDboDir = join(checkoutDir, '.dbo');
274
+ const checkoutProjectDir = join(checkoutDir, '.app');
275
275
 
276
276
  try {
277
277
  // 1. Create checkout dir + minimal config
278
- await mkdir(checkoutDboDir, { recursive: true });
279
- const minConfigPath = join(checkoutDboDir, 'config.json');
278
+ await mkdir(checkoutProjectDir, { recursive: true });
279
+ const minConfigPath = join(checkoutProjectDir, 'config.json');
280
280
  let configExists = false;
281
281
  try { await access(minConfigPath); configExists = true; } catch {}
282
282
  if (!configExists) {
@@ -286,20 +286,31 @@ export async function syncDependencies(options = {}) {
286
286
  }
287
287
 
288
288
  // 2. Symlink credentials
289
- await symlinkCredentials(parentDboDir, checkoutDboDir);
289
+ await symlinkCredentials(parentProjectDir, checkoutProjectDir);
290
290
 
291
291
  // 3. Staleness check (unless --force or --schema)
292
292
  if (!forceAll) {
293
- let isStale = true;
293
+ // Also check if the checkout is essentially empty (only .app/ exists) —
294
+ // a previous clone may have failed or been cleaned up, leaving just config
295
+ let checkoutEmpty = true;
294
296
  try {
295
- isStale = await checkDependencyStaleness(shortname, options);
296
- } catch {
297
- // Network unavailableassume stale to attempt clone
298
- }
299
- if (!isStale) {
300
- skipped.push(shortname);
301
- continue;
297
+ const entries = await readdir(checkoutDir);
298
+ checkoutEmpty = entries.every(e => e === '.app' || e.startsWith('.'));
299
+ } catch { /* dir doesn't exist yet treat as empty */ }
300
+
301
+ if (!checkoutEmpty) {
302
+ let isStale = true;
303
+ try {
304
+ isStale = await checkDependencyStaleness(shortname, options);
305
+ } catch {
306
+ // Network unavailable — assume stale to attempt clone
307
+ }
308
+ if (!isStale) {
309
+ skipped.push(shortname);
310
+ continue;
311
+ }
302
312
  }
313
+ // If checkout is empty, always re-clone regardless of timestamp
303
314
  }
304
315
 
305
316
  // 4. Run the clone (quiet — suppress child process output)
@@ -310,18 +321,34 @@ export async function syncDependencies(options = {}) {
310
321
  await execFn(checkoutDir, ['clone', '--app', shortname, '--force', '--yes', '--no-deps'], { quiet: true });
311
322
  }
312
323
 
313
- // 5. Read _LastUpdated from checkout's app.json and persist
324
+ // 5. Read _LastUpdated from checkout's .app/<shortname>.metadata.json and persist
314
325
  try {
315
- const appJsonPath = join(checkoutDir, 'app.json');
316
- const appJson = JSON.parse(await readFile(appJsonPath, 'utf8'));
317
- const ts = appJson._LastUpdated || appJson.LastUpdated || null;
326
+ const metaFile = join(checkoutDir, '.app', `${shortname}.metadata.json`);
327
+ const appMeta = JSON.parse(await readFile(metaFile, 'utf8'));
328
+ const ts = appMeta._LastUpdated || appMeta.LastUpdated || null;
318
329
  if (ts) await setDependencyLastUpdated(shortname, ts);
319
330
  } catch { /* can't read _LastUpdated — that's OK */ }
320
331
 
332
+ // 6. Clean up legacy and unnecessary files from dependency checkouts.
333
+ // - Legacy .dbo/ layout from pre-restructure clones
334
+ // - Root app.json/schema.json (now inside .app/)
335
+ // - package.json, manifest.json, .gitignore (not needed for deps)
336
+ // - src/, test/, trash/ (deps are not development projects)
337
+ try {
338
+ const legacyDbo = join(checkoutDir, '.dbo');
339
+ try { await access(legacyDbo); await rm(legacyDbo, { recursive: true }); } catch {}
340
+ for (const f of ['app.json', 'schema.json', 'package.json', 'manifest.json', '.gitignore']) {
341
+ try { await access(join(checkoutDir, f)); await rm(join(checkoutDir, f)); } catch {}
342
+ }
343
+ for (const d of ['src', 'test', 'trash']) {
344
+ try { await access(join(checkoutDir, d)); await rm(join(checkoutDir, d), { recursive: true }); } catch {}
345
+ }
346
+ } catch { /* ignore cleanup errors */ }
347
+
321
348
  synced.push(shortname);
322
349
  } catch (err) {
323
350
  failed.push(shortname);
324
- if (options.verbose) log.warn(` Dependency "${shortname}" failed: ${err.message}`);
351
+ log.warn(` Dependency "${shortname}" failed: ${err.message}`);
325
352
  }
326
353
  }
327
354
 
@@ -2,7 +2,7 @@ import { readFile, writeFile, mkdir } from 'fs/promises';
2
2
  import { join, relative, extname, basename } from 'path';
3
3
  import { log } from './logger.js';
4
4
 
5
- const DEPLOY_CONFIG_FILE = '.dbo/deploy_config.json';
5
+ const DEPLOY_CONFIG_FILE = '.app/deploy_config.json';
6
6
 
7
7
  function deployConfigPath() {
8
8
  return join(process.cwd(), DEPLOY_CONFIG_FILE);
@@ -13,7 +13,7 @@ function sortedKeys(obj) {
13
13
  }
14
14
 
15
15
  /**
16
- * Load .dbo/deploy_config.json. Returns { deployments: {} } if missing.
16
+ * Load .app/deploy_config.json. Returns { deployments: {} } if missing.
17
17
  * Throws on malformed JSON (do not silently recreate — would lose existing entries).
18
18
  */
19
19
  export async function loadDeployConfig() {
@@ -27,10 +27,10 @@ export async function loadDeployConfig() {
27
27
  }
28
28
 
29
29
  /**
30
- * Write .dbo/deploy_config.json with alphabetically sorted deployment keys.
30
+ * Write .app/deploy_config.json with alphabetically sorted deployment keys.
31
31
  */
32
32
  export async function saveDeployConfig(config) {
33
- await mkdir(join(process.cwd(), '.dbo'), { recursive: true });
33
+ await mkdir(join(process.cwd(), '.app'), { recursive: true });
34
34
  const sorted = { ...config, deployments: sortedKeys(config.deployments || {}) };
35
35
  await writeFile(deployConfigPath(), JSON.stringify(sorted, null, 2) + '\n');
36
36
  }
@@ -1,24 +1,23 @@
1
1
  import { readFile, writeFile, stat, utimes } from 'fs/promises';
2
2
  import { join } from 'path';
3
- import { loadConfig, loadTransactionKeyPreset } from './config.js';
3
+ import { loadConfig, loadTransactionKeyPreset, appMetadataPath } from './config.js';
4
4
  import { log } from './logger.js';
5
5
 
6
- const appJsonPath = () => join(process.cwd(), 'app.json');
7
-
8
- /** Read _domain from app.json. Returns null if file missing or field absent. */
6
+ /** Read _domain from the metadata file. Returns null if file missing or field absent. */
9
7
  export async function readAppJsonDomain() {
10
8
  try {
11
- const obj = JSON.parse(await readFile(appJsonPath(), 'utf8'));
9
+ const path = await appMetadataPath();
10
+ const obj = JSON.parse(await readFile(path, 'utf8'));
12
11
  return obj._domain || null;
13
12
  } catch { return null; }
14
13
  }
15
14
 
16
15
  /**
17
- * Write _domain to app.json, preserving mtime so git doesn't see a spurious change.
18
- * No-op if app.json does not exist.
16
+ * Write _domain to the metadata file, preserving mtime so git doesn't see a spurious change.
17
+ * No-op if the metadata file does not exist.
19
18
  */
20
19
  export async function writeAppJsonDomain(domain) {
21
- const path = appJsonPath();
20
+ const path = await appMetadataPath();
22
21
  let originalStat;
23
22
  try { originalStat = await stat(path); } catch { return; }
24
23
  const obj = JSON.parse(await readFile(path, 'utf8'));
@@ -29,7 +28,7 @@ export async function writeAppJsonDomain(domain) {
29
28
 
30
29
  /**
31
30
  * Check whether newDomain differs from the project reference domain.
32
- * Reference = app.json._domain (authoritative) → fallback: config.json.domain.
31
+ * Reference = metadata._domain (authoritative) → fallback: config.json.domain.
33
32
  * If reference is absent entirely, returns { changed: false } (no warning).
34
33
  *
35
34
  * Returns { changed: false } or { changed: true, proceed: bool }.
@@ -131,7 +131,18 @@ export function detectLegacyDotUid(filename) {
131
131
  * @returns {string}
132
132
  */
133
133
  export function buildMetaFilename(naturalBase, uid) {
134
- return `${naturalBase}.metadata~${uid}.json`;
134
+ // Guard: strip any trailing .metadata suffix(es) and ~uid fragments from naturalBase
135
+ // to prevent double-metadata filenames (e.g., "app.metadata.metadata~app.json")
136
+ let base = naturalBase;
137
+ const metaParsed = parseMetaFilename(base + '.json');
138
+ if (metaParsed) {
139
+ base = metaParsed.naturalBase;
140
+ }
141
+ // Strip all trailing .metadata (handles single, double, triple, etc.)
142
+ while (base.endsWith('.metadata')) {
143
+ base = base.substring(0, base.length - 9);
144
+ }
145
+ return `${base}.metadata~${uid}.json`;
135
146
  }
136
147
 
137
148
  /**
package/src/lib/ignore.js CHANGED
@@ -12,11 +12,10 @@ const DEFAULT_FILE_CONTENT = `# DBO CLI ignore patterns
12
12
  # (gitignore-style syntax — works like .gitignore)
13
13
 
14
14
  # DBO internal
15
- .dbo/
15
+ .app/
16
16
  .dboignore
17
17
  *.dboio.json
18
- app.json
19
- .dbo/dependencies/
18
+ app_dependencies/
20
19
 
21
20
  # Editor / IDE / OS
22
21
  .DS_Store
package/src/lib/insert.js CHANGED
@@ -71,7 +71,7 @@ const TEMPLATE_DEFAULTS = {
71
71
  };
72
72
 
73
73
  /**
74
- * Seed .dbo/metadata_schema.json with default fields for the given entity
74
+ * Seed metadata schema with default fields for the given entity
75
75
  * (and any other missing core entities) if an entry doesn't already exist.
76
76
  */
77
77
  export async function seedMetadataTemplate() {
@@ -1,10 +1,9 @@
1
1
  import { readFile, writeFile, readdir, access } from 'fs/promises';
2
2
  import { join, relative, basename, extname } from 'path';
3
3
  import { EXTENSION_DESCRIPTORS_DIR, ENTITY_DIR_NAMES } from './structure.js';
4
+ import { metadataSchemaPath } from './config.js';
4
5
  import inquirer from 'inquirer';
5
6
 
6
- export const METADATA_SCHEMA_FILE = '.dbo/metadata_schema.json';
7
-
8
7
  const STATIC_DIRECTIVE_MAP = {
9
8
  docs: { entity: 'extension', descriptor: 'documentation' },
10
9
  };
@@ -67,33 +66,29 @@ export function resolveDirective(filePath) {
67
66
  }
68
67
 
69
68
  /**
70
- * Load metadata schema from .dbo/metadata_schema.json.
69
+ * Load metadata schema from .app/<shortName>.metadata_schema.json.
71
70
  */
72
71
  export async function loadMetadataSchema() {
73
72
  try {
74
- const raw = await readFile(METADATA_SCHEMA_FILE, 'utf8');
75
- try {
76
- return JSON.parse(raw);
77
- } catch {
78
- console.warn('Warning: .dbo/metadata_schema.json is malformed JSON — falling back to generic wizard');
79
- return null;
80
- }
81
- } catch (err) {
82
- if (err.code === 'ENOENT') return {};
83
- return {};
73
+ const path = await metadataSchemaPath();
74
+ const raw = await readFile(path, 'utf8');
75
+ return JSON.parse(raw);
76
+ } catch {
77
+ return null;
84
78
  }
85
79
  }
86
80
 
87
81
  /**
88
- * Save metadata schema to .dbo/metadata_schema.json.
82
+ * Save metadata schema to .app/<shortName>.metadata_schema.json.
89
83
  */
90
- export async function saveMetadataSchema(templates) {
91
- await writeFile(METADATA_SCHEMA_FILE, JSON.stringify(templates, null, 2) + '\n');
84
+ export async function saveMetadataSchema(data) {
85
+ const path = await metadataSchemaPath();
86
+ await writeFile(path, JSON.stringify(data, null, 2) + '\n');
92
87
  }
93
88
 
94
89
  /**
95
90
  * Merge extension descriptor definitions from dependency metadata_schema.json files.
96
- * For each dependency in .dbo/dependencies/<name>/, reads its .dbo/metadata_schema.json
91
+ * For each dependency in app_dependencies/<name>/, reads its .app/<name>.metadata_schema.json
97
92
  * and copies any extension descriptor entries that don't already exist locally.
98
93
  *
99
94
  * @param {Object} localSchema - The current project's metadata_schema object (mutated in place)
@@ -102,7 +97,7 @@ export async function saveMetadataSchema(templates) {
102
97
  export async function mergeDescriptorSchemaFromDependencies(localSchema) {
103
98
  if (!localSchema) return false;
104
99
 
105
- const depsRoot = join('.dbo', 'dependencies');
100
+ const depsRoot = join(process.cwd(), 'app_dependencies');
106
101
  let depNames;
107
102
  try {
108
103
  depNames = await readdir(depsRoot);
@@ -113,7 +108,7 @@ export async function mergeDescriptorSchemaFromDependencies(localSchema) {
113
108
  let merged = false;
114
109
 
115
110
  for (const depName of depNames) {
116
- const depSchemaPath = join(depsRoot, depName, '.dbo', 'metadata_schema.json');
111
+ const depSchemaPath = join(depsRoot, depName, '.app', `${depName}.metadata_schema.json`);
117
112
  let depSchema;
118
113
  try {
119
114
  depSchema = JSON.parse(await readFile(depSchemaPath, 'utf8'));
@@ -489,4 +484,3 @@ export function generateMetadataFromSchema(schema, existing = {}) {
489
484
  // Backward-compat re-exports
490
485
  export const loadMetadataTemplates = loadMetadataSchema;
491
486
  export const saveMetadataTemplates = saveMetadataSchema;
492
- export const METADATA_TEMPLATES_FILE = METADATA_SCHEMA_FILE;
@@ -3,7 +3,7 @@ import { relative, basename, extname } from 'path';
3
3
  import { EXTENSION_DESCRIPTORS_DIR, ENTITY_DIR_NAMES } from './structure.js';
4
4
  import inquirer from 'inquirer';
5
5
 
6
- const METADATA_TEMPLATES_FILE = '.dbo/metadata_templates.json';
6
+ const METADATA_TEMPLATES_FILE = '.app/metadata_templates.json';
7
7
 
8
8
  const STATIC_DIRECTIVE_MAP = {
9
9
  docs: { entity: 'extension', descriptor: 'documentation' },
@@ -67,7 +67,7 @@ export function resolveDirective(filePath) {
67
67
  }
68
68
 
69
69
  /**
70
- * Load metadata templates from .dbo/metadata_templates.json.
70
+ * Load metadata templates from .app/metadata_templates.json.
71
71
  */
72
72
  export async function loadMetadataTemplates() {
73
73
  try {
@@ -75,7 +75,7 @@ export async function loadMetadataTemplates() {
75
75
  try {
76
76
  return JSON.parse(raw);
77
77
  } catch {
78
- console.warn('Warning: .dbo/metadata_templates.json is malformed JSON — falling back to generic wizard');
78
+ console.warn('Warning: .app/metadata_templates.json is malformed JSON — falling back to generic wizard');
79
79
  return null;
80
80
  }
81
81
  } catch (err) {
@@ -85,7 +85,7 @@ export async function loadMetadataTemplates() {
85
85
  }
86
86
 
87
87
  /**
88
- * Save metadata templates to .dbo/metadata_templates.json.
88
+ * Save metadata templates to .app/metadata_templates.json.
89
89
  */
90
90
  export async function saveMetadataTemplates(templates) {
91
91
  await writeFile(METADATA_TEMPLATES_FILE, JSON.stringify(templates, null, 2) + '\n');
@@ -17,7 +17,7 @@ export async function runPendingMigrations(options = {}) {
17
17
  // --no-migrate suppresses for this invocation
18
18
  if (options.migrate === false) return;
19
19
 
20
- // No .dbo/ project in this directory — nothing to migrate
20
+ // No .app/ project in this directory — nothing to migrate
21
21
  if (!(await isInitialized())) return;
22
22
 
23
23
  // Discover migration files
@@ -104,6 +104,6 @@ export async function handleModifyKeyError() {
104
104
  }
105
105
 
106
106
  await saveAppModifyKey(key);
107
- log.info(' ModifyKey saved to .dbo/config.json for future use.');
107
+ log.info(' ModifyKey saved to .app/config.json for future use.');
108
108
  return { modifyKey: key, cancel: false };
109
109
  }
@@ -3,6 +3,7 @@ import { join } from 'path';
3
3
  import { SCAFFOLD_DIRS } from './structure.js';
4
4
  import { log } from './logger.js';
5
5
  import { applyTrashIcon } from './tagging.js';
6
+ import { appMetadataPath } from './config.js';
6
7
 
7
8
  /**
8
9
  * Scaffold the standard DBO project directory structure in cwd.
@@ -37,21 +38,12 @@ export async function scaffoldProjectDirs(cwd = process.cwd()) {
37
38
  // Best-effort: apply trash icon to the trash directory
38
39
  await applyTrashIcon(join(cwd, 'trash'));
39
40
 
40
- // Create app.json if absent
41
- const appJsonPath = join(cwd, 'app.json');
42
- try {
43
- await access(appJsonPath);
44
- } catch {
45
- await writeFile(appJsonPath, '{}\n');
46
- created.push('app.json');
47
- }
48
-
49
41
  // Create manifest.json if absent
50
42
  const manifestPath = join(cwd, 'manifest.json');
51
43
  try {
52
44
  await access(manifestPath);
53
45
  } catch {
54
- // Try to resolve values from local app.json; fall back to empty strings
46
+ // Try to resolve values from app metadata file; fall back to empty strings
55
47
  let appName = '';
56
48
  let shortName = '';
57
49
  let description = '';
@@ -59,7 +51,8 @@ export async function scaffoldProjectDirs(cwd = process.cwd()) {
59
51
  let domain = '';
60
52
 
61
53
  try {
62
- const appData = JSON.parse(await readFile(appJsonPath, 'utf8'));
54
+ const appMetaPath = await appMetadataPath();
55
+ const appData = JSON.parse(await readFile(appMetaPath, 'utf8'));
63
56
  appName = appData.Name || '';
64
57
  shortName = appData.ShortName || '';
65
58
  description = appData.Description || '';
@@ -74,7 +67,7 @@ export async function scaffoldProjectDirs(cwd = process.cwd()) {
74
67
  }
75
68
  }
76
69
  }
77
- } catch { /* app.json missing or unparseable — use empty defaults */ }
70
+ } catch { /* metadata file missing or unparseable — use empty defaults */ }
78
71
 
79
72
  const manifest = {
80
73
  name: `${appName} | ${domain}`,