@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/config.js CHANGED
@@ -1,35 +1,41 @@
1
1
  import { readFile, writeFile, mkdir, access, chmod, unlink } from 'fs/promises';
2
2
  import { join } from 'path';
3
+ import { homedir } from 'os';
3
4
  import { log } from './logger.js';
4
5
 
5
- const DBO_DIR = '.dbo';
6
+ const APP_DIR = '.app';
7
+ const LEGACY_DBO_DIR = '.dbo'; // kept for migration bootstrap only
6
8
  const CONFIG_FILE = 'config.json';
7
- const CONFIG_LOCAL_FILE = 'config.local.json';
9
+ const CONFIG_LOCAL_FILE = 'settings.json';
8
10
  const CREDENTIALS_FILE = 'credentials.json';
9
11
  const COOKIES_FILE = 'cookies.txt';
10
12
  const SYNCHRONIZE_FILE = 'synchronize.json';
11
- const BASELINE_FILE = '.app_baseline.json';
12
13
  const SCRIPTS_FILE = 'scripts.json';
13
14
  const SCRIPTS_LOCAL_FILE = 'scripts.local.json';
14
15
 
15
- function dboDir() {
16
- return join(process.cwd(), DBO_DIR);
16
+ export function projectDir() {
17
+ return join(process.cwd(), APP_DIR);
18
+ }
19
+
20
+ // Keep private helper for legacy bootstrap:
21
+ function legacyDboDir() {
22
+ return join(process.cwd(), LEGACY_DBO_DIR);
17
23
  }
18
24
 
19
25
  function configPath() {
20
- return join(dboDir(), CONFIG_FILE);
26
+ return join(projectDir(), CONFIG_FILE);
21
27
  }
22
28
 
23
29
  function credentialsPath() {
24
- return join(dboDir(), CREDENTIALS_FILE);
30
+ return join(projectDir(), CREDENTIALS_FILE);
25
31
  }
26
32
 
27
33
  function cookiesPath() {
28
- return join(dboDir(), COOKIES_FILE);
34
+ return join(projectDir(), COOKIES_FILE);
29
35
  }
30
36
 
31
37
  function synchronizePath() {
32
- return join(dboDir(), SYNCHRONIZE_FILE);
38
+ return join(projectDir(), SYNCHRONIZE_FILE);
33
39
  }
34
40
 
35
41
  async function exists(path) {
@@ -42,7 +48,7 @@ async function exists(path) {
42
48
  }
43
49
 
44
50
  export async function isInitialized() {
45
- return exists(dboDir());
51
+ return (await exists(projectDir())) || (await exists(legacyDboDir()));
46
52
  }
47
53
 
48
54
  export async function hasLegacyConfig() {
@@ -65,12 +71,12 @@ export async function readLegacyConfig() {
65
71
  }
66
72
 
67
73
  export async function initConfig(domain) {
68
- await mkdir(dboDir(), { recursive: true });
74
+ await mkdir(projectDir(), { recursive: true });
69
75
  await writeFile(configPath(), JSON.stringify({ domain, dependencies: ['_system'] }, null, 2) + '\n');
70
76
  }
71
77
 
72
78
  export async function saveCredentials(username) {
73
- await mkdir(dboDir(), { recursive: true });
79
+ await mkdir(projectDir(), { recursive: true });
74
80
  // Preserve existing fields (like userId) — never store password
75
81
  let existing = {};
76
82
  try {
@@ -82,7 +88,7 @@ export async function saveCredentials(username) {
82
88
  }
83
89
 
84
90
  export async function saveUserInfo({ userId }) {
85
- await mkdir(dboDir(), { recursive: true });
91
+ await mkdir(projectDir(), { recursive: true });
86
92
  let existing = {};
87
93
  try {
88
94
  existing = JSON.parse(await readFile(credentialsPath(), 'utf8'));
@@ -104,7 +110,7 @@ export async function loadUserInfo() {
104
110
  export async function loadConfig() {
105
111
  const config = { domain: null, username: null, password: null, ServerTimezone: 'UTC' };
106
112
 
107
- // Try .dbo/config.json
113
+ // Try .app/config.json
108
114
  try {
109
115
  const raw = await readFile(configPath(), 'utf8');
110
116
  Object.assign(config, JSON.parse(raw));
@@ -115,7 +121,7 @@ export async function loadConfig() {
115
121
  } catch { /* no config found */ }
116
122
  }
117
123
 
118
- // Try .dbo/credentials.json (username only — password is never stored)
124
+ // Try .app/credentials.json (username only — password is never stored)
119
125
  try {
120
126
  const raw = await readFile(credentialsPath(), 'utf8');
121
127
  const creds = JSON.parse(raw);
@@ -136,8 +142,8 @@ export async function loadConfig() {
136
142
  }
137
143
 
138
144
  export function getCookiesPath() {
139
- // Prefer .dbo/cookies.txt, fall back to legacy .cookies
140
- const dboCookies = join(dboDir(), COOKIES_FILE);
145
+ // Prefer .app/cookies.txt, fall back to legacy .cookies
146
+ const dboCookies = join(projectDir(), COOKIES_FILE);
141
147
  const legacyCookies = join(process.cwd(), '.cookies');
142
148
  return { dbo: dboCookies, legacy: legacyCookies };
143
149
  }
@@ -150,11 +156,11 @@ export async function getActiveCookiesPath() {
150
156
  }
151
157
 
152
158
  /**
153
- * Merge app metadata into .dbo/config.json.
159
+ * Merge app metadata into .app/config.json.
154
160
  * Preserves existing fields; only sets new ones.
155
161
  */
156
162
  export async function updateConfigWithApp({ AppID, AppUID, AppName, AppShortName, ServerTimezone }) {
157
- await mkdir(dboDir(), { recursive: true });
163
+ await mkdir(projectDir(), { recursive: true });
158
164
  let existing = {};
159
165
  try {
160
166
  existing = JSON.parse(await readFile(configPath(), 'utf8'));
@@ -170,7 +176,7 @@ export async function updateConfigWithApp({ AppID, AppUID, AppName, AppShortName
170
176
  // ─── Dependency helpers ───────────────────────────────────────────────────
171
177
 
172
178
  /**
173
- * Get the dependencies array from .dbo/config.json.
179
+ * Get the dependencies array from .app/config.json.
174
180
  * Returns ["_system"] if the key is absent.
175
181
  */
176
182
  export async function getDependencies() {
@@ -188,10 +194,10 @@ export async function getDependencies() {
188
194
 
189
195
  /**
190
196
  * Merge new short-names into the dependencies array (union, no duplicates).
191
- * Persists the result to .dbo/config.json.
197
+ * Persists the result to .app/config.json.
192
198
  */
193
199
  export async function mergeDependencies(shortnames) {
194
- await mkdir(dboDir(), { recursive: true });
200
+ await mkdir(projectDir(), { recursive: true });
195
201
  let existing = {};
196
202
  try {
197
203
  existing = JSON.parse(await readFile(configPath(), 'utf8'));
@@ -207,11 +213,11 @@ export async function mergeDependencies(shortnames) {
207
213
  }
208
214
 
209
215
  /**
210
- * Replace the full dependencies array in .dbo/config.json.
216
+ * Replace the full dependencies array in .app/config.json.
211
217
  * Always ensures _system is present.
212
218
  */
213
219
  export async function setDependencies(shortnames) {
214
- await mkdir(dboDir(), { recursive: true });
220
+ await mkdir(projectDir(), { recursive: true });
215
221
  let existing = {};
216
222
  try {
217
223
  existing = JSON.parse(await readFile(configPath(), 'utf8'));
@@ -222,7 +228,7 @@ export async function setDependencies(shortnames) {
222
228
  }
223
229
 
224
230
  /**
225
- * Get dependencyLastUpdated.<shortname> from .dbo/config.json.
231
+ * Get dependencyLastUpdated.<shortname> from .app/config.json.
226
232
  * Returns null if absent.
227
233
  */
228
234
  export async function getDependencyLastUpdated(shortname) {
@@ -236,10 +242,10 @@ export async function getDependencyLastUpdated(shortname) {
236
242
  }
237
243
 
238
244
  /**
239
- * Set dependencyLastUpdated.<shortname> in .dbo/config.json.
245
+ * Set dependencyLastUpdated.<shortname> in .app/config.json.
240
246
  */
241
247
  export async function setDependencyLastUpdated(shortname, timestamp) {
242
- await mkdir(dboDir(), { recursive: true });
248
+ await mkdir(projectDir(), { recursive: true });
243
249
  let existing = {};
244
250
  try {
245
251
  existing = JSON.parse(await readFile(configPath(), 'utf8'));
@@ -250,7 +256,7 @@ export async function setDependencyLastUpdated(shortname, timestamp) {
250
256
  }
251
257
 
252
258
  /**
253
- * Load app-related fields from .dbo/config.json.
259
+ * Load app-related fields from .app/config.json.
254
260
  */
255
261
  export async function loadAppConfig() {
256
262
  try {
@@ -269,12 +275,12 @@ export async function loadAppConfig() {
269
275
  }
270
276
 
271
277
  /**
272
- * Save clone placement preferences to .dbo/config.json.
278
+ * Save clone placement preferences to .app/config.json.
273
279
  * mediaPlacement: 'fullpath' | 'bin' | 'ask'
274
280
  * contentPlacement: 'path' | 'bin' | 'ask'
275
281
  */
276
282
  export async function saveClonePlacement({ mediaPlacement, contentPlacement }) {
277
- await mkdir(dboDir(), { recursive: true });
283
+ await mkdir(projectDir(), { recursive: true });
278
284
  let existing = {};
279
285
  try {
280
286
  existing = JSON.parse(await readFile(configPath(), 'utf8'));
@@ -285,7 +291,7 @@ export async function saveClonePlacement({ mediaPlacement, contentPlacement }) {
285
291
  }
286
292
 
287
293
  /**
288
- * Load clone placement preferences from .dbo/config.json.
294
+ * Load clone placement preferences from .app/config.json.
289
295
  */
290
296
  export async function loadClonePlacement() {
291
297
  try {
@@ -301,11 +307,11 @@ export async function loadClonePlacement() {
301
307
  }
302
308
 
303
309
  /**
304
- * Save entity-dir filename column preference to .dbo/config.json.
310
+ * Save entity-dir filename column preference to .app/config.json.
305
311
  * Key format: "<EntityType>FilenameCol" (e.g., "ExtensionFilenameCol")
306
312
  */
307
313
  export async function saveEntityDirPreference(entityKey, filenameCol) {
308
- await mkdir(dboDir(), { recursive: true });
314
+ await mkdir(projectDir(), { recursive: true });
309
315
  let existing = {};
310
316
  try {
311
317
  existing = JSON.parse(await readFile(configPath(), 'utf8'));
@@ -317,7 +323,7 @@ export async function saveEntityDirPreference(entityKey, filenameCol) {
317
323
  }
318
324
 
319
325
  /**
320
- * Load entity-dir filename column preference from .dbo/config.json.
326
+ * Load entity-dir filename column preference from .app/config.json.
321
327
  */
322
328
  export async function loadEntityDirPreference(entityKey) {
323
329
  try {
@@ -339,7 +345,7 @@ export async function loadEntityDirPreference(entityKey) {
339
345
  * false means user explicitly chose not to extract that column
340
346
  */
341
347
  export async function saveEntityContentExtractions(entityKey, extractions) {
342
- await mkdir(dboDir(), { recursive: true });
348
+ await mkdir(projectDir(), { recursive: true });
343
349
  let existing = {};
344
350
  try {
345
351
  existing = JSON.parse(await readFile(configPath(), 'utf8'));
@@ -351,7 +357,7 @@ export async function saveEntityContentExtractions(entityKey, extractions) {
351
357
  }
352
358
 
353
359
  /**
354
- * Load content extraction preferences for an entity type from .dbo/config.json.
360
+ * Load content extraction preferences for an entity type from .app/config.json.
355
361
  *
356
362
  * @param {string} entityKey - Entity type (e.g., 'extension', 'site')
357
363
  * @returns {Object|null} - Map of column names to extensions, or null if not saved
@@ -368,13 +374,13 @@ export async function loadEntityContentExtractions(entityKey) {
368
374
  }
369
375
 
370
376
  /**
371
- * Save collision resolutions to .dbo/config.json.
377
+ * Save collision resolutions to .app/config.json.
372
378
  * Maps file paths to the UID of the record the user chose to keep.
373
379
  *
374
380
  * @param {Object} resolutions - { "Bins/app/file.css": { keepUID: "abc", keepEntity: "content" } }
375
381
  */
376
382
  export async function saveCollisionResolutions(resolutions) {
377
- await mkdir(dboDir(), { recursive: true });
383
+ await mkdir(projectDir(), { recursive: true });
378
384
  let existing = {};
379
385
  try {
380
386
  existing = JSON.parse(await readFile(configPath(), 'utf8'));
@@ -384,7 +390,7 @@ export async function saveCollisionResolutions(resolutions) {
384
390
  }
385
391
 
386
392
  /**
387
- * Load collision resolutions from .dbo/config.json.
393
+ * Load collision resolutions from .app/config.json.
388
394
  *
389
395
  * @returns {Object} - { "Bins/app/file.css": { keepUID: "abc", keepEntity: "content" } }
390
396
  */
@@ -402,7 +408,7 @@ export async function loadCollisionResolutions() {
402
408
  * Save user profile fields (FirstName, LastName, Email) into credentials.json.
403
409
  */
404
410
  export async function saveUserProfile({ FirstName, LastName, Email }) {
405
- await mkdir(dboDir(), { recursive: true });
411
+ await mkdir(projectDir(), { recursive: true });
406
412
  let existing = {};
407
413
  try {
408
414
  existing = JSON.parse(await readFile(credentialsPath(), 'utf8'));
@@ -446,7 +452,7 @@ export async function loadSynchronize() {
446
452
  * Save synchronize.json.
447
453
  */
448
454
  export async function saveSynchronize(data) {
449
- await mkdir(dboDir(), { recursive: true });
455
+ await mkdir(projectDir(), { recursive: true });
450
456
  await writeFile(synchronizePath(), JSON.stringify(data, null, 2) + '\n');
451
457
  }
452
458
 
@@ -474,15 +480,15 @@ export async function removeDeleteEntry(uid) {
474
480
  }
475
481
 
476
482
  /**
477
- * Remove a @metaPath reference from app.json children arrays.
483
+ * Remove a @metaPath reference from the app metadata file children arrays.
478
484
  */
479
485
  export async function removeAppJsonReference(metaPath) {
480
- const appJsonPath = join(process.cwd(), 'app.json');
486
+ const appJsonPath = await appMetadataPath();
481
487
  let appJson;
482
488
  try {
483
489
  appJson = JSON.parse(await readFile(appJsonPath, 'utf8'));
484
490
  } catch {
485
- return; // no app.json
491
+ return; // no metadata file
486
492
  }
487
493
 
488
494
  if (!appJson.children) return;
@@ -504,14 +510,18 @@ export async function removeAppJsonReference(metaPath) {
504
510
  }
505
511
  }
506
512
 
507
- // ─── config.local.json (per-user, gitignored) ────────────────────────────
513
+ // ─── config.local.json (global ~/.dbo/settings.json) ────────────────────
508
514
 
509
515
  function configLocalPath() {
510
- return join(dboDir(), CONFIG_LOCAL_FILE);
516
+ return join(homedir(), '.dbo', CONFIG_LOCAL_FILE);
517
+ }
518
+
519
+ async function ensureGlobalDboDir() {
520
+ await mkdir(join(homedir(), '.dbo'), { recursive: true });
511
521
  }
512
522
 
513
523
  /**
514
- * Load config.local.json (per-user settings including plugin scopes).
524
+ * Load ~/.dbo/settings.json (per-user settings including plugin scopes).
515
525
  * Returns empty structure if file doesn't exist.
516
526
  */
517
527
  export async function loadLocalConfig() {
@@ -524,10 +534,10 @@ export async function loadLocalConfig() {
524
534
  }
525
535
 
526
536
  /**
527
- * Save config.local.json.
537
+ * Save ~/.dbo/settings.json.
528
538
  */
529
539
  export async function saveLocalConfig(data) {
530
- await mkdir(dboDir(), { recursive: true });
540
+ await ensureGlobalDboDir();
531
541
  await writeFile(configLocalPath(), JSON.stringify(data, null, 2) + '\n');
532
542
  }
533
543
 
@@ -609,35 +619,44 @@ export async function getAllPluginScopes() {
609
619
  return result;
610
620
  }
611
621
 
612
- // ─── Migration tracking (config.local.json._completedMigrations) ──────────
622
+ // ─── Migration tracking (keyed by project path in ~/.dbo/settings.json) ──
613
623
 
614
624
  /**
615
- * Load the list of completed migration IDs from .dbo/config.local.json.
625
+ * Load the list of completed migration IDs from ~/.dbo/settings.json.
626
+ * Keyed by project cwd for multi-project support.
616
627
  * Returns an empty array if the file does not exist or the key is absent.
617
628
  * @returns {Promise<string[]>}
618
629
  */
619
630
  export async function loadCompletedMigrations() {
620
631
  try {
621
- const local = await loadLocalConfig();
622
- const ids = local._completedMigrations;
623
- return Array.isArray(ids) ? ids : [];
632
+ const raw = await readFile(configLocalPath(), 'utf8');
633
+ const settings = JSON.parse(raw);
634
+ const key = process.cwd();
635
+ const projectMigrations = settings._completedMigrations?.[key];
636
+ return Array.isArray(projectMigrations) ? projectMigrations : [];
624
637
  } catch {
625
638
  return [];
626
639
  }
627
640
  }
628
641
 
629
642
  /**
630
- * Append a migration ID to .dbo/config.local.json._completedMigrations.
643
+ * Append a migration ID to ~/.dbo/settings.json._completedMigrations[cwd].
631
644
  * Deduplicates: if the ID is already present, no-op.
632
645
  * @param {string} id - Three-digit migration ID, e.g. '001'
633
646
  */
634
647
  export async function saveCompletedMigration(id) {
635
- const local = await loadLocalConfig();
636
- const existing = new Set(Array.isArray(local._completedMigrations) ? local._completedMigrations : []);
637
- if (existing.has(id)) return; // already recorded — idempotent
648
+ await ensureGlobalDboDir();
649
+ let settings = {};
650
+ try {
651
+ settings = JSON.parse(await readFile(configLocalPath(), 'utf8'));
652
+ } catch { /* first write */ }
653
+ if (!settings._completedMigrations) settings._completedMigrations = {};
654
+ const key = process.cwd();
655
+ const existing = new Set(Array.isArray(settings._completedMigrations[key]) ? settings._completedMigrations[key] : []);
656
+ if (existing.has(id)) return;
638
657
  existing.add(id);
639
- local._completedMigrations = [...existing].sort();
640
- await saveLocalConfig(local);
658
+ settings._completedMigrations[key] = [...existing].sort();
659
+ await writeFile(configLocalPath(), JSON.stringify(settings, null, 2) + '\n');
641
660
  }
642
661
 
643
662
  // ─── Output Hierarchy Filename Preferences ────────────────────────────────
@@ -677,7 +696,7 @@ export async function loadOutputFilenamePreference(entityKey) {
677
696
  export async function saveOutputFilenamePreference(entityKey, columnName) {
678
697
  const configKey = OUTPUT_FILENAME_CONFIG_KEYS[entityKey];
679
698
  if (!configKey) return;
680
- await mkdir(dboDir(), { recursive: true });
699
+ await mkdir(projectDir(), { recursive: true });
681
700
  let existing = {};
682
701
  try { existing = JSON.parse(await readFile(configPath(), 'utf8')); } catch {}
683
702
  existing[configKey] = columnName;
@@ -687,11 +706,11 @@ export async function saveOutputFilenamePreference(entityKey, columnName) {
687
706
  // ─── AppModifyKey ─────────────────────────────────────────────────────────
688
707
 
689
708
  /**
690
- * Save AppModifyKey to .dbo/config.json.
709
+ * Save AppModifyKey to .app/config.json.
691
710
  * Pass null to remove the key (e.g. when server no longer has one).
692
711
  */
693
712
  export async function saveAppModifyKey(modifyKey) {
694
- await mkdir(dboDir(), { recursive: true });
713
+ await mkdir(projectDir(), { recursive: true });
695
714
  let existing = {};
696
715
  try { existing = JSON.parse(await readFile(configPath(), 'utf8')); } catch {}
697
716
  if (modifyKey != null) existing.AppModifyKey = modifyKey;
@@ -700,7 +719,7 @@ export async function saveAppModifyKey(modifyKey) {
700
719
  }
701
720
 
702
721
  /**
703
- * Load AppModifyKey from .dbo/config.json.
722
+ * Load AppModifyKey from .app/config.json.
704
723
  * Returns the key string or null if not set.
705
724
  */
706
725
  export async function loadAppModifyKey() {
@@ -713,11 +732,11 @@ export async function loadAppModifyKey() {
713
732
  // ─── TransactionKeyPreset ─────────────────────────────────────────────────
714
733
 
715
734
  /**
716
- * Save TransactionKeyPreset to .dbo/config.json.
735
+ * Save TransactionKeyPreset to .app/config.json.
717
736
  * @param {'RowUID'|'RowID'} preset
718
737
  */
719
738
  export async function saveTransactionKeyPreset(preset) {
720
- await mkdir(dboDir(), { recursive: true });
739
+ await mkdir(projectDir(), { recursive: true });
721
740
  let existing = {};
722
741
  try { existing = JSON.parse(await readFile(configPath(), 'utf8')); } catch {}
723
742
  existing.TransactionKeyPreset = preset;
@@ -725,7 +744,7 @@ export async function saveTransactionKeyPreset(preset) {
725
744
  }
726
745
 
727
746
  /**
728
- * Load TransactionKeyPreset from .dbo/config.json.
747
+ * Load TransactionKeyPreset from .app/config.json.
729
748
  * Returns 'RowUID', 'RowID', or null if not set.
730
749
  */
731
750
  export async function loadTransactionKeyPreset() {
@@ -738,11 +757,11 @@ export async function loadTransactionKeyPreset() {
738
757
  // ─── TicketSuggestionOutput ────────────────────────────────────────────────
739
758
 
740
759
  /**
741
- * Save TicketSuggestionOutput to .dbo/config.json.
760
+ * Save TicketSuggestionOutput to .app/config.json.
742
761
  * This is the output UID used to fetch ticket suggestions.
743
762
  */
744
763
  export async function saveTicketSuggestionOutput(outputUid) {
745
- await mkdir(dboDir(), { recursive: true });
764
+ await mkdir(projectDir(), { recursive: true });
746
765
  let existing = {};
747
766
  try { existing = JSON.parse(await readFile(configPath(), 'utf8')); } catch {}
748
767
  if (outputUid != null) existing.TicketSuggestionOutput = outputUid;
@@ -751,7 +770,7 @@ export async function saveTicketSuggestionOutput(outputUid) {
751
770
  }
752
771
 
753
772
  /**
754
- * Load TicketSuggestionOutput from .dbo/config.json.
773
+ * Load TicketSuggestionOutput from .app/config.json.
755
774
  * Returns the output UID string or null if not set.
756
775
  */
757
776
  export async function loadTicketSuggestionOutput() {
@@ -808,86 +827,118 @@ export async function ensureGitignore(patterns) {
808
827
  for (const p of toAdd) log.dim(` Added ${p} to .gitignore`);
809
828
  }
810
829
 
811
- // ─── Baseline (.dbo/.app_baseline.json) ───────────────────────────────────
830
+ // ─── Baseline (.app/<shortName>.json) ─────────────────────────────────────
831
+
832
+ function sanitizeShortName(name) {
833
+ if (!name) return 'app';
834
+ return String(name).replace(/[/\\:*?"<>|]/g, '-');
835
+ }
836
+
837
+ export async function baselinePath() {
838
+ try {
839
+ const raw = await readFile(configPath(), 'utf8');
840
+ const cfg = JSON.parse(raw);
841
+ return join(projectDir(), sanitizeShortName(cfg.AppShortName) + '.json');
842
+ } catch {
843
+ return join(projectDir(), 'app.json');
844
+ }
845
+ }
812
846
 
813
- export function baselinePath() {
814
- return join(dboDir(), BASELINE_FILE);
847
+ /** Path to .app/<shortName>.metadata.json (the former root app.json with @references). */
848
+ export async function appMetadataPath() {
849
+ try {
850
+ const raw = await readFile(configPath(), 'utf8');
851
+ const cfg = JSON.parse(raw);
852
+ return join(projectDir(), sanitizeShortName(cfg.AppShortName) + '.metadata.json');
853
+ } catch {
854
+ return join(projectDir(), 'app.metadata.json');
855
+ }
856
+ }
857
+
858
+ /** Path to .app/<shortName>.metadata_schema.json. */
859
+ export async function metadataSchemaPath() {
860
+ try {
861
+ const raw = await readFile(configPath(), 'utf8');
862
+ const cfg = JSON.parse(raw);
863
+ return join(projectDir(), sanitizeShortName(cfg.AppShortName) + '.metadata_schema.json');
864
+ } catch {
865
+ return join(projectDir(), 'app.metadata_schema.json');
866
+ }
815
867
  }
816
868
 
817
869
  /**
818
- * Check if baseline file (.app.json) exists.
870
+ * Check if baseline file exists.
819
871
  */
820
872
  export async function hasBaseline() {
821
- return exists(baselinePath());
873
+ return exists(await baselinePath());
822
874
  }
823
875
 
824
876
  /**
825
- * Load .dbo/.app_baseline.json baseline file.
826
- * Auto-migrates from legacy root .app.json if the new path does not exist.
877
+ * Load .app/<shortName>.json baseline file.
878
+ * Auto-migrates from legacy paths if the new path does not exist.
827
879
  */
828
880
  export async function loadAppJsonBaseline() {
829
- const newPath = baselinePath();
830
-
831
- // Legacy migration: root .app.json .dbo/.app_baseline.json
832
- if (!(await exists(newPath))) {
833
- const legacyPath = join(process.cwd(), '.app.json');
834
- if (await exists(legacyPath)) {
835
- let parsed;
836
- try {
837
- parsed = JSON.parse(await readFile(legacyPath, 'utf8'));
838
- } catch {
839
- log.warn('Could not migrate .app.json — file is not valid JSON. Delete it manually and run "dbo clone" to recreate the baseline.');
840
- return null;
841
- }
842
- await mkdir(dboDir(), { recursive: true });
843
- await writeFile(newPath, JSON.stringify(parsed, null, 2) + '\n');
844
- try { await chmod(newPath, 0o444); } catch { /* ignore */ }
845
- try {
846
- await unlink(legacyPath);
847
- } catch {
848
- log.warn('Migrated baseline but could not delete root .app.json — please remove it manually.');
849
- }
850
- log.dim('Migrated .app.json → .dbo/.app_baseline.json (system-managed baseline)');
881
+ const newPath = await baselinePath();
882
+ if (await exists(newPath)) {
883
+ try { return JSON.parse(await readFile(newPath, 'utf8')); } catch { return null; }
884
+ }
885
+
886
+ // Legacy: .dbo/.app_baseline.json (post-baseline-relocation, pre-bin-restructure)
887
+ const legacyDboPath = join(legacyDboDir(), '.app_baseline.json');
888
+ if (await exists(legacyDboPath)) {
889
+ try {
890
+ const parsed = JSON.parse(await readFile(legacyDboPath, 'utf8'));
891
+ await saveAppJsonBaseline(parsed); // writes to new path
851
892
  return parsed;
852
- }
893
+ } catch { return null; }
853
894
  }
854
895
 
855
- try {
856
- const raw = await readFile(newPath, 'utf8');
857
- return JSON.parse(raw);
858
- } catch {
859
- return null;
896
+ // Legacy: root .app.json (very old format)
897
+ const rootLegacy = join(process.cwd(), '.app.json');
898
+ if (await exists(rootLegacy)) {
899
+ let parsed;
900
+ try {
901
+ parsed = JSON.parse(await readFile(rootLegacy, 'utf8'));
902
+ } catch {
903
+ log.warn('Could not migrate .app.json — file is not valid JSON. Delete it manually and run "dbo clone" to recreate the baseline.');
904
+ return null;
905
+ }
906
+ await mkdir(projectDir(), { recursive: true });
907
+ await saveAppJsonBaseline(parsed);
908
+ try {
909
+ await unlink(rootLegacy);
910
+ } catch {
911
+ log.warn('Migrated baseline but could not delete root .app.json — please remove it manually.');
912
+ }
913
+ log.dim('Migrated .app.json → .app/<shortName>.json (system-managed baseline)');
914
+ return parsed;
860
915
  }
916
+
917
+ return null;
861
918
  }
862
919
 
863
920
  /**
864
- * Save .dbo/.app_baseline.json baseline file.
921
+ * Save .app/<shortName>.json baseline file.
865
922
  * Temporarily widens permissions before writing (chmod 0o644),
866
923
  * then restores read-only (chmod 0o444) after writing.
867
924
  */
868
925
  export async function saveAppJsonBaseline(data) {
869
- await mkdir(dboDir(), { recursive: true });
870
-
871
- try {
872
- await chmod(baselinePath(), 0o644);
873
- } catch { /* file doesn't exist yet — first write */ }
874
-
875
- await writeFile(baselinePath(), JSON.stringify(data, null, 2) + '\n');
876
-
877
- try {
878
- await chmod(baselinePath(), 0o444);
879
- } catch {
880
- log.warn('⚠ Could not set baseline file permissions — ensure .dbo/.app_baseline.json is not manually edited');
926
+ await mkdir(projectDir(), { recursive: true });
927
+ const path = await baselinePath();
928
+ try { await chmod(path, 0o644); } catch { /* first write */ }
929
+ await writeFile(path, JSON.stringify(data, null, 2) + '\n');
930
+ try { await chmod(path, 0o444); } catch {
931
+ log.warn('⚠ Could not set baseline file permissions');
881
932
  }
882
933
  }
883
934
 
884
935
  /**
885
- * Save the clone source to .dbo/config.json.
936
+ * Save the clone source to .app/config.json.
886
937
  * "default" = fetched from server via AppShortName.
887
938
  * Any other value = explicit local file path or URL provided by the user.
888
939
  */
889
940
  export async function saveCloneSource(source) {
890
- await mkdir(dboDir(), { recursive: true });
941
+ await mkdir(projectDir(), { recursive: true });
891
942
  let existing = {};
892
943
  try {
893
944
  existing = JSON.parse(await readFile(configPath(), 'utf8'));
@@ -897,7 +948,7 @@ export async function saveCloneSource(source) {
897
948
  }
898
949
 
899
950
  /**
900
- * Load the stored clone source from .dbo/config.json.
951
+ * Load the stored clone source from .app/config.json.
901
952
  * Returns null if not set.
902
953
  */
903
954
  export async function loadCloneSource() {
@@ -916,7 +967,7 @@ export async function loadCloneSource() {
916
967
  * Config key: "Extension_<descriptor>_FilenameCol"
917
968
  */
918
969
  export async function saveDescriptorFilenamePreference(descriptor, columnName) {
919
- await mkdir(dboDir(), { recursive: true });
970
+ await mkdir(projectDir(), { recursive: true });
920
971
  let cfg = {};
921
972
  try { cfg = JSON.parse(await readFile(configPath(), 'utf8')); } catch {}
922
973
  if (columnName === null) {
@@ -940,7 +991,7 @@ export async function loadDescriptorFilenamePreference(descriptor) {
940
991
  * Value: { "ColName": "css", "Other": false, ... }
941
992
  */
942
993
  export async function saveDescriptorContentExtractions(descriptor, extractions) {
943
- await mkdir(dboDir(), { recursive: true });
994
+ await mkdir(projectDir(), { recursive: true });
944
995
  let cfg = {};
945
996
  try { cfg = JSON.parse(await readFile(configPath(), 'utf8')); } catch {}
946
997
  if (extractions === null) {
@@ -963,7 +1014,7 @@ export async function loadDescriptorContentExtractions(descriptor) {
963
1014
  * @param {'inline'|'root'|null} placement — null clears the key
964
1015
  */
965
1016
  export async function saveExtensionDocumentationMDPlacement(placement) {
966
- await mkdir(dboDir(), { recursive: true });
1017
+ await mkdir(projectDir(), { recursive: true });
967
1018
  let cfg = {};
968
1019
  try { cfg = JSON.parse(await readFile(configPath(), 'utf8')); } catch {}
969
1020
  if (placement === null) {
@@ -982,18 +1033,18 @@ export async function loadExtensionDocumentationMDPlacement() {
982
1033
  } catch { return null; }
983
1034
  }
984
1035
 
985
- // ─── Script Hooks (.dbo/scripts.json, .dbo/scripts.local.json) ────────────
1036
+ // ─── Script Hooks (.app/scripts.json, .app/scripts.local.json) ────────────
986
1037
 
987
1038
  function scriptsPath() {
988
- return join(dboDir(), SCRIPTS_FILE);
1039
+ return join(projectDir(), SCRIPTS_FILE);
989
1040
  }
990
1041
 
991
1042
  function scriptsLocalPath() {
992
- return join(dboDir(), SCRIPTS_LOCAL_FILE);
1043
+ return join(projectDir(), SCRIPTS_LOCAL_FILE);
993
1044
  }
994
1045
 
995
1046
  /**
996
- * Load .dbo/scripts.json. Returns parsed object or null if missing.
1047
+ * Load .app/scripts.json. Returns parsed object or null if missing.
997
1048
  * Throws SyntaxError with clear message if JSON is malformed.
998
1049
  */
999
1050
  export async function loadScripts() {
@@ -1001,12 +1052,12 @@ export async function loadScripts() {
1001
1052
  let raw;
1002
1053
  try { raw = await readFile(path, 'utf8'); } catch { return null; }
1003
1054
  try { return JSON.parse(raw); } catch (err) {
1004
- throw new SyntaxError(`Invalid JSON in .dbo/scripts.json: ${err.message}`);
1055
+ throw new SyntaxError(`Invalid JSON in .app/scripts.json: ${err.message}`);
1005
1056
  }
1006
1057
  }
1007
1058
 
1008
1059
  /**
1009
- * Load .dbo/scripts.local.json (gitignored per-user overrides).
1060
+ * Load .app/scripts.local.json (gitignored per-user overrides).
1010
1061
  * Returns parsed object or null if missing.
1011
1062
  * Throws SyntaxError with clear message if JSON is malformed.
1012
1063
  */
@@ -1015,7 +1066,7 @@ export async function loadScriptsLocal() {
1015
1066
  let raw;
1016
1067
  try { raw = await readFile(path, 'utf8'); } catch { return null; }
1017
1068
  try { return JSON.parse(raw); } catch (err) {
1018
- throw new SyntaxError(`Invalid JSON in .dbo/scripts.local.json: ${err.message}`);
1069
+ throw new SyntaxError(`Invalid JSON in .app/scripts.local.json: ${err.message}`);
1019
1070
  }
1020
1071
  }
1021
1072
 
@@ -1027,7 +1078,7 @@ export async function loadScriptsLocal() {
1027
1078
  */
1028
1079
  export async function loadTagConfig() {
1029
1080
  try {
1030
- const raw = await readFile(join(process.cwd(), DBO_DIR, CONFIG_FILE), 'utf8');
1081
+ const raw = await readFile(join(process.cwd(), APP_DIR, CONFIG_FILE), 'utf8');
1031
1082
  const config = JSON.parse(raw);
1032
1083
  return { tagFiles: config.tagFiles !== false };
1033
1084
  } catch {
@@ -1040,9 +1091,9 @@ export async function loadTagConfig() {
1040
1091
  * @param {boolean} enabled
1041
1092
  */
1042
1093
  export async function saveTagConfig(enabled) {
1043
- const configPath = join(process.cwd(), DBO_DIR, CONFIG_FILE);
1094
+ const cfgPath = join(process.cwd(), APP_DIR, CONFIG_FILE);
1044
1095
  let config = {};
1045
- try { config = JSON.parse(await readFile(configPath, 'utf8')); } catch {}
1096
+ try { config = JSON.parse(await readFile(cfgPath, 'utf8')); } catch {}
1046
1097
  config.tagFiles = enabled;
1047
- await writeFile(configPath, JSON.stringify(config, null, 2));
1098
+ await writeFile(cfgPath, JSON.stringify(config, null, 2));
1048
1099
  }