@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.
- 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 +236 -97
- 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/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
|
|
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 = '
|
|
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
|
|
16
|
-
return join(process.cwd(),
|
|
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(
|
|
26
|
+
return join(projectDir(), CONFIG_FILE);
|
|
21
27
|
}
|
|
22
28
|
|
|
23
29
|
function credentialsPath() {
|
|
24
|
-
return join(
|
|
30
|
+
return join(projectDir(), CREDENTIALS_FILE);
|
|
25
31
|
}
|
|
26
32
|
|
|
27
33
|
function cookiesPath() {
|
|
28
|
-
return join(
|
|
34
|
+
return join(projectDir(), COOKIES_FILE);
|
|
29
35
|
}
|
|
30
36
|
|
|
31
37
|
function synchronizePath() {
|
|
32
|
-
return join(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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 .
|
|
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 .
|
|
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 .
|
|
140
|
-
const dboCookies = join(
|
|
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 .
|
|
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(
|
|
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 .
|
|
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 .
|
|
197
|
+
* Persists the result to .app/config.json.
|
|
192
198
|
*/
|
|
193
199
|
export async function mergeDependencies(shortnames) {
|
|
194
|
-
await mkdir(
|
|
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 .
|
|
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(
|
|
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 .
|
|
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 .
|
|
245
|
+
* Set dependencyLastUpdated.<shortname> in .app/config.json.
|
|
240
246
|
*/
|
|
241
247
|
export async function setDependencyLastUpdated(shortname, timestamp) {
|
|
242
|
-
await mkdir(
|
|
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 .
|
|
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 .
|
|
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(
|
|
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 .
|
|
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 .
|
|
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(
|
|
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 .
|
|
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(
|
|
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 .
|
|
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 .
|
|
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(
|
|
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 .
|
|
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(
|
|
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(
|
|
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
|
|
483
|
+
* Remove a @metaPath reference from the app metadata file children arrays.
|
|
478
484
|
*/
|
|
479
485
|
export async function removeAppJsonReference(metaPath) {
|
|
480
|
-
const appJsonPath =
|
|
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
|
|
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 (
|
|
513
|
+
// ─── config.local.json (global ~/.dbo/settings.json) ────────────────────
|
|
508
514
|
|
|
509
515
|
function configLocalPath() {
|
|
510
|
-
return join(
|
|
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
|
|
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
|
|
537
|
+
* Save ~/.dbo/settings.json.
|
|
528
538
|
*/
|
|
529
539
|
export async function saveLocalConfig(data) {
|
|
530
|
-
await
|
|
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 (
|
|
622
|
+
// ─── Migration tracking (keyed by project path in ~/.dbo/settings.json) ──
|
|
613
623
|
|
|
614
624
|
/**
|
|
615
|
-
* Load the list of completed migration IDs from
|
|
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
|
|
622
|
-
const
|
|
623
|
-
|
|
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
|
|
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
|
-
|
|
636
|
-
|
|
637
|
-
|
|
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
|
-
|
|
640
|
-
await
|
|
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(
|
|
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 .
|
|
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(
|
|
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 .
|
|
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 .
|
|
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(
|
|
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 .
|
|
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 .
|
|
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(
|
|
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 .
|
|
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 (.
|
|
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
|
-
|
|
814
|
-
|
|
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
|
|
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 .
|
|
826
|
-
* Auto-migrates from legacy
|
|
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
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
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
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
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 .
|
|
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(
|
|
870
|
-
|
|
871
|
-
try {
|
|
872
|
-
|
|
873
|
-
|
|
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 .
|
|
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(
|
|
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 .
|
|
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(
|
|
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(
|
|
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(
|
|
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 (.
|
|
1036
|
+
// ─── Script Hooks (.app/scripts.json, .app/scripts.local.json) ────────────
|
|
986
1037
|
|
|
987
1038
|
function scriptsPath() {
|
|
988
|
-
return join(
|
|
1039
|
+
return join(projectDir(), SCRIPTS_FILE);
|
|
989
1040
|
}
|
|
990
1041
|
|
|
991
1042
|
function scriptsLocalPath() {
|
|
992
|
-
return join(
|
|
1043
|
+
return join(projectDir(), SCRIPTS_LOCAL_FILE);
|
|
993
1044
|
}
|
|
994
1045
|
|
|
995
1046
|
/**
|
|
996
|
-
* Load .
|
|
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 .
|
|
1055
|
+
throw new SyntaxError(`Invalid JSON in .app/scripts.json: ${err.message}`);
|
|
1005
1056
|
}
|
|
1006
1057
|
}
|
|
1007
1058
|
|
|
1008
1059
|
/**
|
|
1009
|
-
* Load .
|
|
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 .
|
|
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(),
|
|
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
|
|
1094
|
+
const cfgPath = join(process.cwd(), APP_DIR, CONFIG_FILE);
|
|
1044
1095
|
let config = {};
|
|
1045
|
-
try { config = JSON.parse(await readFile(
|
|
1096
|
+
try { config = JSON.parse(await readFile(cfgPath, 'utf8')); } catch {}
|
|
1046
1097
|
config.tagFiles = enabled;
|
|
1047
|
-
await writeFile(
|
|
1098
|
+
await writeFile(cfgPath, JSON.stringify(config, null, 2));
|
|
1048
1099
|
}
|