@dboio/cli 0.17.0 → 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.
- package/README.md +111 -85
- package/package.json +1 -1
- package/plugins/claude/dbo/docs/dbo-cli-readme.md +111 -85
- package/src/commands/build.js +3 -3
- package/src/commands/clone.js +205 -71
- package/src/commands/deploy.js +3 -3
- package/src/commands/init.js +11 -11
- package/src/commands/install.js +3 -3
- package/src/commands/login.js +2 -2
- package/src/commands/mv.js +15 -15
- package/src/commands/pull.js +1 -1
- package/src/commands/push.js +193 -14
- package/src/commands/rm.js +2 -2
- package/src/commands/run.js +4 -4
- package/src/commands/status.js +1 -1
- package/src/commands/sync.js +2 -2
- package/src/lib/config.js +186 -135
- package/src/lib/delta.js +119 -17
- package/src/lib/dependencies.js +51 -24
- package/src/lib/deploy-config.js +4 -4
- package/src/lib/domain-guard.js +8 -9
- package/src/lib/filenames.js +12 -1
- package/src/lib/ignore.js +2 -3
- package/src/lib/insert.js +1 -1
- package/src/lib/metadata-schema.js +14 -20
- package/src/lib/metadata-templates.js +4 -4
- package/src/lib/migrations.js +1 -1
- package/src/lib/modify-key.js +1 -1
- package/src/lib/scaffold.js +5 -12
- package/src/lib/schema.js +67 -37
- package/src/lib/structure.js +6 -6
- package/src/lib/tagging.js +2 -2
- package/src/lib/ticketing.js +3 -7
- package/src/lib/toe-stepping.js +5 -5
- package/src/lib/transaction-key.js +1 -1
- package/src/migrations/004-rename-output-files.js +2 -2
- package/src/migrations/005-rename-output-metadata.js +2 -2
- package/src/migrations/006-remove-uid-companion-filenames.js +1 -1
- package/src/migrations/007-natural-entity-companion-filenames.js +1 -1
- package/src/migrations/008-metadata-uid-in-suffix.js +1 -1
- package/src/migrations/009-fix-media-collision-metadata-names.js +1 -1
- package/src/migrations/010-delete-paren-media-orphans.js +1 -1
- 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
|
-
//
|
|
49
|
-
//
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
const
|
|
57
|
-
if (
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
+
}
|
package/src/lib/dependencies.js
CHANGED
|
@@ -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
|
|
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)}
|
|
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
|
|
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}
|
|
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
|
|
256
|
-
const depsRoot = join(
|
|
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
|
|
274
|
+
const checkoutProjectDir = join(checkoutDir, '.app');
|
|
275
275
|
|
|
276
276
|
try {
|
|
277
277
|
// 1. Create checkout dir + minimal config
|
|
278
|
-
await mkdir(
|
|
279
|
-
const minConfigPath = join(
|
|
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(
|
|
289
|
+
await symlinkCredentials(parentProjectDir, checkoutProjectDir);
|
|
290
290
|
|
|
291
291
|
// 3. Staleness check (unless --force or --schema)
|
|
292
292
|
if (!forceAll) {
|
|
293
|
-
|
|
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
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
if (!
|
|
300
|
-
|
|
301
|
-
|
|
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
|
|
316
|
-
const
|
|
317
|
-
const ts =
|
|
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
|
-
|
|
351
|
+
log.warn(` Dependency "${shortname}" failed: ${err.message}`);
|
|
325
352
|
}
|
|
326
353
|
}
|
|
327
354
|
|
package/src/lib/deploy-config.js
CHANGED
|
@@ -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 = '.
|
|
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 .
|
|
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 .
|
|
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(), '.
|
|
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
|
}
|
package/src/lib/domain-guard.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
|
18
|
-
* No-op if
|
|
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 =
|
|
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 =
|
|
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 }.
|
package/src/lib/filenames.js
CHANGED
|
@@ -131,7 +131,18 @@ export function detectLegacyDotUid(filename) {
|
|
|
131
131
|
* @returns {string}
|
|
132
132
|
*/
|
|
133
133
|
export function buildMetaFilename(naturalBase, uid) {
|
|
134
|
-
|
|
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
|
-
.
|
|
15
|
+
.app/
|
|
16
16
|
.dboignore
|
|
17
17
|
*.dboio.json
|
|
18
|
-
|
|
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
|
|
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 .
|
|
69
|
+
* Load metadata schema from .app/<shortName>.metadata_schema.json.
|
|
71
70
|
*/
|
|
72
71
|
export async function loadMetadataSchema() {
|
|
73
72
|
try {
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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 .
|
|
82
|
+
* Save metadata schema to .app/<shortName>.metadata_schema.json.
|
|
89
83
|
*/
|
|
90
|
-
export async function saveMetadataSchema(
|
|
91
|
-
|
|
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
|
|
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(
|
|
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, '.
|
|
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 = '.
|
|
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 .
|
|
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: .
|
|
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 .
|
|
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');
|
package/src/lib/migrations.js
CHANGED
|
@@ -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 .
|
|
20
|
+
// No .app/ project in this directory — nothing to migrate
|
|
21
21
|
if (!(await isInitialized())) return;
|
|
22
22
|
|
|
23
23
|
// Discover migration files
|
package/src/lib/modify-key.js
CHANGED
|
@@ -104,6 +104,6 @@ export async function handleModifyKeyError() {
|
|
|
104
104
|
}
|
|
105
105
|
|
|
106
106
|
await saveAppModifyKey(key);
|
|
107
|
-
log.info(' ModifyKey saved to .
|
|
107
|
+
log.info(' ModifyKey saved to .app/config.json for future use.');
|
|
108
108
|
return { modifyKey: key, cancel: false };
|
|
109
109
|
}
|
package/src/lib/scaffold.js
CHANGED
|
@@ -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
|
|
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
|
|
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 { /*
|
|
70
|
+
} catch { /* metadata file missing or unparseable — use empty defaults */ }
|
|
78
71
|
|
|
79
72
|
const manifest = {
|
|
80
73
|
name: `${appName} | ${domain}`,
|