@dboio/cli 0.4.2 → 0.5.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/src/lib/config.js CHANGED
@@ -4,8 +4,10 @@ import { log } from './logger.js';
4
4
 
5
5
  const DBO_DIR = '.dbo';
6
6
  const CONFIG_FILE = 'config.json';
7
+ const CONFIG_LOCAL_FILE = 'config.local.json';
7
8
  const CREDENTIALS_FILE = 'credentials.json';
8
9
  const COOKIES_FILE = 'cookies.txt';
10
+ const SYNCHRONIZE_FILE = 'synchronize.json';
9
11
 
10
12
  function dboDir() {
11
13
  return join(process.cwd(), DBO_DIR);
@@ -23,6 +25,10 @@ function cookiesPath() {
23
25
  return join(dboDir(), COOKIES_FILE);
24
26
  }
25
27
 
28
+ function synchronizePath() {
29
+ return join(dboDir(), SYNCHRONIZE_FILE);
30
+ }
31
+
26
32
  async function exists(path) {
27
33
  try {
28
34
  await access(path);
@@ -212,6 +218,36 @@ export async function loadClonePlacement() {
212
218
  }
213
219
  }
214
220
 
221
+ /**
222
+ * Save entity-dir filename column preference to .dbo/config.json.
223
+ * Key format: "<EntityType>FilenameCol" (e.g., "ExtensionFilenameCol")
224
+ */
225
+ export async function saveEntityDirPreference(entityKey, filenameCol) {
226
+ await mkdir(dboDir(), { recursive: true });
227
+ let existing = {};
228
+ try {
229
+ existing = JSON.parse(await readFile(configPath(), 'utf8'));
230
+ } catch { /* no existing config */ }
231
+ // Capitalize first letter for config key: extension → ExtensionFilenameCol
232
+ const configKey = entityKey.charAt(0).toUpperCase() + entityKey.slice(1) + 'FilenameCol';
233
+ existing[configKey] = filenameCol;
234
+ await writeFile(configPath(), JSON.stringify(existing, null, 2) + '\n');
235
+ }
236
+
237
+ /**
238
+ * Load entity-dir filename column preference from .dbo/config.json.
239
+ */
240
+ export async function loadEntityDirPreference(entityKey) {
241
+ try {
242
+ const raw = await readFile(configPath(), 'utf8');
243
+ const config = JSON.parse(raw);
244
+ const configKey = entityKey.charAt(0).toUpperCase() + entityKey.slice(1) + 'FilenameCol';
245
+ return config[configKey] || null;
246
+ } catch {
247
+ return null;
248
+ }
249
+ }
250
+
215
251
  /**
216
252
  * Save user profile fields (FirstName, LastName, Email) into credentials.json.
217
253
  */
@@ -244,6 +280,165 @@ export async function loadUserProfile() {
244
280
  }
245
281
  }
246
282
 
283
+ /**
284
+ * Load synchronize.json (staging file for pending server operations).
285
+ */
286
+ export async function loadSynchronize() {
287
+ try {
288
+ const raw = await readFile(synchronizePath(), 'utf8');
289
+ return JSON.parse(raw);
290
+ } catch {
291
+ return { delete: [], edit: [], add: [] };
292
+ }
293
+ }
294
+
295
+ /**
296
+ * Save synchronize.json.
297
+ */
298
+ export async function saveSynchronize(data) {
299
+ await mkdir(dboDir(), { recursive: true });
300
+ await writeFile(synchronizePath(), JSON.stringify(data, null, 2) + '\n');
301
+ }
302
+
303
+ /**
304
+ * Stage a delete entry in synchronize.json. Deduplicates by UID.
305
+ */
306
+ export async function addDeleteEntry(entry) {
307
+ const data = await loadSynchronize();
308
+ const idx = data.delete.findIndex(e => e.UID === entry.UID);
309
+ if (idx >= 0) {
310
+ data.delete[idx] = entry;
311
+ } else {
312
+ data.delete.push(entry);
313
+ }
314
+ await saveSynchronize(data);
315
+ }
316
+
317
+ /**
318
+ * Remove a delete entry from synchronize.json by UID.
319
+ */
320
+ export async function removeDeleteEntry(uid) {
321
+ const data = await loadSynchronize();
322
+ data.delete = data.delete.filter(e => e.UID !== uid);
323
+ await saveSynchronize(data);
324
+ }
325
+
326
+ /**
327
+ * Remove a @metaPath reference from app.json children arrays.
328
+ */
329
+ export async function removeAppJsonReference(metaPath) {
330
+ const appJsonPath = join(process.cwd(), 'app.json');
331
+ let appJson;
332
+ try {
333
+ appJson = JSON.parse(await readFile(appJsonPath, 'utf8'));
334
+ } catch {
335
+ return; // no app.json
336
+ }
337
+
338
+ if (!appJson.children) return;
339
+
340
+ const ref = `@${metaPath}`;
341
+ let changed = false;
342
+
343
+ for (const [key, arr] of Object.entries(appJson.children)) {
344
+ if (!Array.isArray(arr)) continue;
345
+ const filtered = arr.filter(entry => entry !== ref);
346
+ if (filtered.length !== arr.length) {
347
+ appJson.children[key] = filtered;
348
+ changed = true;
349
+ }
350
+ }
351
+
352
+ if (changed) {
353
+ await writeFile(appJsonPath, JSON.stringify(appJson, null, 2) + '\n');
354
+ }
355
+ }
356
+
357
+ // ─── config.local.json (per-user, gitignored) ────────────────────────────
358
+
359
+ function configLocalPath() {
360
+ return join(dboDir(), CONFIG_LOCAL_FILE);
361
+ }
362
+
363
+ /**
364
+ * Load config.local.json (per-user settings including plugin scopes).
365
+ * Returns empty structure if file doesn't exist.
366
+ */
367
+ export async function loadLocalConfig() {
368
+ try {
369
+ const raw = await readFile(configLocalPath(), 'utf8');
370
+ return JSON.parse(raw);
371
+ } catch {
372
+ return { plugins: {} };
373
+ }
374
+ }
375
+
376
+ /**
377
+ * Save config.local.json.
378
+ */
379
+ export async function saveLocalConfig(data) {
380
+ await mkdir(dboDir(), { recursive: true });
381
+ await writeFile(configLocalPath(), JSON.stringify(data, null, 2) + '\n');
382
+ }
383
+
384
+ /**
385
+ * Get the stored scope for a plugin by name (without extension).
386
+ * @param {string} pluginName - Plugin name without extension
387
+ * @param {string} [category='claudecommands'] - Plugin category
388
+ * @returns {Promise<'project' | 'global' | null>}
389
+ */
390
+ export async function getPluginScope(pluginName, category = 'claudecommands') {
391
+ const config = await loadLocalConfig();
392
+ return config.plugins?.[category]?.[pluginName] || null;
393
+ }
394
+
395
+ /**
396
+ * Set the scope for a plugin by name.
397
+ * @param {string} pluginName - Plugin name without extension
398
+ * @param {'project' | 'global'} scope
399
+ * @param {string} [category='claudecommands'] - Plugin category
400
+ */
401
+ export async function setPluginScope(pluginName, scope, category = 'claudecommands') {
402
+ const config = await loadLocalConfig();
403
+ if (!config.plugins) config.plugins = {};
404
+ if (!config.plugins[category]) config.plugins[category] = {};
405
+ config.plugins[category][pluginName] = scope;
406
+ await saveLocalConfig(config);
407
+ }
408
+
409
+ /**
410
+ * Get all stored plugin scopes for a category.
411
+ * Returns object mapping plugin names to their scope strings.
412
+ * @param {string} [category='claudecommands'] - Plugin category
413
+ */
414
+ export async function getAllPluginScopes(category = 'claudecommands') {
415
+ const config = await loadLocalConfig();
416
+ return config.plugins?.[category] || {};
417
+ }
418
+
419
+ // ─── Gitignore ────────────────────────────────────────────────────────────
420
+
421
+ /**
422
+ * Remove a specific pattern from .gitignore.
423
+ * Used when moving a plugin from project to global scope.
424
+ */
425
+ export async function removeFromGitignore(pattern) {
426
+ const gitignorePath = join(process.cwd(), '.gitignore');
427
+ let content = '';
428
+ try {
429
+ content = await readFile(gitignorePath, 'utf8');
430
+ } catch {
431
+ return; // no .gitignore
432
+ }
433
+
434
+ if (!content.includes(pattern)) return;
435
+
436
+ const lines = content.split('\n');
437
+ const filtered = lines.filter(line => line.trim() !== pattern.trim());
438
+ await writeFile(gitignorePath, filtered.join('\n'));
439
+ log.dim(` Removed ${pattern} from .gitignore`);
440
+ }
441
+
247
442
  /**
248
443
  * Ensure patterns are in .gitignore. Creates .gitignore if it doesn't exist.
249
444
  */