@dboio/cli 0.19.4 → 0.20.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 +69 -16
- package/bin/dbo.js +5 -0
- package/package.json +1 -1
- package/plugins/claude/dbo/.claude-plugin/plugin.json +6 -1
- package/plugins/claude/dbo/commands/dbo.md +39 -12
- package/plugins/claude/dbo/docs/dbo-cli-readme.md +69 -16
- package/plugins/claude/dbo/skills/cookbook/SKILL.md +162 -0
- package/plugins/claude/dbo/skills/white-paper/SKILL.md +49 -8
- package/plugins/claude/dbo/skills/white-paper/references/api-reference.md +1 -1
- package/plugins/claude/track/.claude-plugin/plugin.json +1 -1
- package/src/commands/adopt.js +69 -14
- package/src/commands/clone.js +451 -87
- package/src/commands/init.js +2 -2
- package/src/commands/input.js +2 -2
- package/src/commands/login.js +3 -3
- package/src/commands/push.js +203 -54
- package/src/commands/status.js +15 -7
- package/src/lib/config.js +137 -10
- package/src/lib/filenames.js +54 -66
- package/src/lib/ignore.js +3 -0
- package/src/lib/insert.js +29 -45
- package/src/lib/structure.js +23 -8
- package/src/lib/ticketing.js +9 -8
- package/src/migrations/008-metadata-uid-in-suffix.js +4 -2
- package/src/migrations/009-fix-media-collision-metadata-names.js +9 -3
- package/src/migrations/013-remove-uid-from-meta-filenames.js +117 -0
- package/src/migrations/014-entity-dir-to-data-source.js +68 -0
package/src/commands/init.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
2
|
import { mkdir, writeFile, access } from 'fs/promises';
|
|
3
3
|
import { join } from 'path';
|
|
4
|
-
import { isInitialized, hasLegacyConfig, readLegacyConfig, initConfig, saveCredentials, ensureGitignore, saveTransactionKeyPreset, saveTicketSuggestionOutput } from '../lib/config.js';
|
|
4
|
+
import { isInitialized, hasLegacyConfig, readLegacyConfig, initConfig, saveCredentials, ensureGitignore, DEFAULT_GITIGNORE_ENTRIES, saveTransactionKeyPreset, saveTicketSuggestionOutput } from '../lib/config.js';
|
|
5
5
|
import { installOrUpdateClaudeCommands } from './install.js';
|
|
6
6
|
import { scaffoldProjectDirs, logScaffoldResult } from '../lib/scaffold.js';
|
|
7
7
|
import { createDboignore } from '../lib/ignore.js';
|
|
@@ -108,7 +108,7 @@ export const initCommand = new Command('init')
|
|
|
108
108
|
}
|
|
109
109
|
|
|
110
110
|
// Ensure sensitive files are gitignored
|
|
111
|
-
await ensureGitignore(
|
|
111
|
+
await ensureGitignore(DEFAULT_GITIGNORE_ENTRIES);
|
|
112
112
|
|
|
113
113
|
const createdIgnore = await createDboignore();
|
|
114
114
|
if (createdIgnore) log.dim(' Created .dboignore');
|
package/src/commands/input.js
CHANGED
|
@@ -15,7 +15,7 @@ export const inputCommand = new Command('input')
|
|
|
15
15
|
.description('Submit CRUD operations to DBO.io (add, edit, delete records)')
|
|
16
16
|
.requiredOption('-d, --data <expr>', 'DBO input expression (repeatable)', collect, [])
|
|
17
17
|
.option('-f, --file <field=@path>', 'File attachment for multipart upload (repeatable)', collect, [])
|
|
18
|
-
.option('-C, --confirm
|
|
18
|
+
.option('-C, --confirm [value]', 'Commit changes: true (default) or false for validation only', 'true')
|
|
19
19
|
.option('--ticket <id>', 'Override ticket ID (_OverrideTicketID)')
|
|
20
20
|
.option('--modify-key <key>', 'Provide ModifyKey directly (skips interactive prompt)')
|
|
21
21
|
.option('--row-key <type>', 'Row key type (RowUID or RowID) — no-op for -d passthrough, available for consistency')
|
|
@@ -32,7 +32,7 @@ export const inputCommand = new Command('input')
|
|
|
32
32
|
const client = new DboClient({ domain: options.domain, verbose: options.verbose });
|
|
33
33
|
|
|
34
34
|
const extraParams = {};
|
|
35
|
-
extraParams['_confirm'] = options.confirm;
|
|
35
|
+
extraParams['_confirm'] = String(options.confirm);
|
|
36
36
|
if (options.ticket) extraParams['_OverrideTicketID'] = options.ticket;
|
|
37
37
|
if (options.login) extraParams['_login'] = 'true';
|
|
38
38
|
if (options.transactional) extraParams['_transactional'] = 'true';
|
package/src/commands/login.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
|
-
import { loadConfig, initConfig, saveUserInfo, saveUserProfile, ensureGitignore } from '../lib/config.js';
|
|
2
|
+
import { loadConfig, initConfig, saveUserInfo, saveUserProfile, ensureGitignore, DEFAULT_GITIGNORE_ENTRIES } from '../lib/config.js';
|
|
3
3
|
import { DboClient } from '../lib/client.js';
|
|
4
4
|
import { log } from '../lib/logger.js';
|
|
5
5
|
|
|
@@ -49,7 +49,7 @@ export async function performLogin(domain, knownUsername) {
|
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
log.success(`Authenticated as ${username} on ${await client.getDomain()}`);
|
|
52
|
-
await ensureGitignore(
|
|
52
|
+
await ensureGitignore(DEFAULT_GITIGNORE_ENTRIES);
|
|
53
53
|
|
|
54
54
|
// Fetch and store user info (non-critical)
|
|
55
55
|
try {
|
|
@@ -143,7 +143,7 @@ export const loginCommand = new Command('login')
|
|
|
143
143
|
log.success(`Authenticated as ${username} on ${await client.getDomain()}`);
|
|
144
144
|
|
|
145
145
|
// Ensure sensitive files are gitignored
|
|
146
|
-
await ensureGitignore(
|
|
146
|
+
await ensureGitignore(DEFAULT_GITIGNORE_ENTRIES);
|
|
147
147
|
|
|
148
148
|
// Fetch current user info to store ID for future submissions
|
|
149
149
|
try {
|
package/src/commands/push.js
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
|
-
import { readFile, stat, writeFile, rename as fsRename, mkdir, access, utimes } from 'fs/promises';
|
|
3
|
-
import { join, dirname, basename, extname, relative } from 'path';
|
|
2
|
+
import { readFile, stat, writeFile, rename as fsRename, mkdir, access, utimes, readdir } from 'fs/promises';
|
|
3
|
+
import { join, dirname, basename, extname, relative, sep } from 'path';
|
|
4
4
|
import { DboClient } from '../lib/client.js';
|
|
5
5
|
import { buildInputBody, checkSubmitErrors, getSessionUserOverride } from '../lib/input-parser.js';
|
|
6
6
|
import { formatResponse, formatError } from '../lib/formatter.js';
|
|
7
7
|
import { log } from '../lib/logger.js';
|
|
8
8
|
import { shouldSkipColumn } from '../lib/columns.js';
|
|
9
|
-
import { loadConfig, loadAppConfig, loadSynchronize, saveSynchronize, loadAppJsonBaseline, saveAppJsonBaseline, hasBaseline, loadScripts, loadScriptsLocal, addDeleteEntry } from '../lib/config.js';
|
|
9
|
+
import { loadConfig, loadAppConfig, loadSynchronize, saveSynchronize, loadAppJsonBaseline, saveAppJsonBaseline, hasBaseline, loadScripts, loadScriptsLocal, addDeleteEntry, loadRootContentFiles } from '../lib/config.js';
|
|
10
10
|
import { mergeScriptsConfig, resolveHooks, buildHookEnv, runBuildLifecycle, runPushLifecycle } from '../lib/scripts.js';
|
|
11
11
|
import { checkStoredTicket, applyStoredTicketToSubmission, clearRecordTicket, clearGlobalTicket } from '../lib/ticketing.js';
|
|
12
12
|
import { checkModifyKey, isModifyKeyError, handleModifyKeyError } from '../lib/modify-key.js';
|
|
13
13
|
import { resolveTransactionKey } from '../lib/transaction-key.js';
|
|
14
14
|
import { setFileTimestamps, parseServerDate } from '../lib/timestamps.js';
|
|
15
|
-
import { stripUidFromFilename, renameToUidConvention,
|
|
15
|
+
import { stripUidFromFilename, renameToUidConvention, isMetadataFile, parseMetaFilename, findMetadataForCompanion } from '../lib/filenames.js';
|
|
16
16
|
import { findMetadataFiles, findFileInProject, findByUID } from '../lib/diff.js';
|
|
17
17
|
import { loadIgnore } from '../lib/ignore.js';
|
|
18
18
|
import { detectChangedColumns, findBaselineEntry, detectOutputChanges, detectEntityChildrenChanges, getAllUserColumns, isReference, resolveReferencePath, detectBinChanges, synthesizeBinMetadata } from '../lib/delta.js';
|
|
@@ -343,6 +343,33 @@ async function pushSingleFile(filePath, client, options, modifyKey = null, trans
|
|
|
343
343
|
metaPath = found;
|
|
344
344
|
try { meta = JSON.parse(await readFile(metaPath, 'utf8')); } catch {}
|
|
345
345
|
}
|
|
346
|
+
if (!meta) {
|
|
347
|
+
// Last resort: project-wide search for cross-directory @reference.
|
|
348
|
+
// Handles cases where the companion file is in a different directory from its metadata
|
|
349
|
+
// (e.g. docs/Operator_Claude.md → lib/extension/Documentation/Operator_Claude.metadata~uid.json).
|
|
350
|
+
const absoluteFilePath = filePath.startsWith('/') ? filePath : join(process.cwd(), filePath);
|
|
351
|
+
const ig = await loadIgnore();
|
|
352
|
+
const allMetaFiles = await findMetadataFiles(process.cwd(), ig);
|
|
353
|
+
for (const candidatePath of allMetaFiles) {
|
|
354
|
+
try {
|
|
355
|
+
const candidateMeta = JSON.parse(await readFile(candidatePath, 'utf8'));
|
|
356
|
+
const metaDir = dirname(candidatePath);
|
|
357
|
+
const cols = [...(candidateMeta._companionReferenceColumns || candidateMeta._contentColumns || [])];
|
|
358
|
+
if (candidateMeta._mediaFile) cols.push('_mediaFile');
|
|
359
|
+
for (const col of cols) {
|
|
360
|
+
const ref = candidateMeta[col];
|
|
361
|
+
if (!ref || !String(ref).startsWith('@')) continue;
|
|
362
|
+
const resolved = resolveAtReference(String(ref).substring(1), metaDir);
|
|
363
|
+
if (resolved === absoluteFilePath) {
|
|
364
|
+
metaPath = candidatePath;
|
|
365
|
+
meta = candidateMeta;
|
|
366
|
+
break;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
} catch { /* skip unreadable */ }
|
|
370
|
+
if (meta) break;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
346
373
|
if (!meta) {
|
|
347
374
|
// AUTO-ADD DISABLED: local-file-first approach is being replaced by server-first workflow.
|
|
348
375
|
// New records should be created via the DBO REST API first, then `dbo pull` to populate locally.
|
|
@@ -399,70 +426,187 @@ async function pushSingleFile(filePath, client, options, modifyKey = null, trans
|
|
|
399
426
|
}
|
|
400
427
|
// ── End script hooks ────────────────────────────────────────────────
|
|
401
428
|
|
|
402
|
-
await pushFromMetadata(meta, metaPath, client, options, null, modifyKey, transactionKey);
|
|
429
|
+
const success = await pushFromMetadata(meta, metaPath, client, options, null, modifyKey, transactionKey);
|
|
430
|
+
if (success) {
|
|
431
|
+
const baseline = await loadAppJsonBaseline();
|
|
432
|
+
if (baseline) {
|
|
433
|
+
await updateBaselineAfterPush(baseline, [{ meta, metaPath, changedColumns: null }]);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
return success;
|
|
403
437
|
}
|
|
404
438
|
/**
|
|
405
|
-
*
|
|
406
|
-
*
|
|
407
|
-
*
|
|
439
|
+
* Metadata templates for known root content files.
|
|
440
|
+
* Keyed by filename (case-sensitive). Unknown files in rootContentFiles
|
|
441
|
+
* get a sensible derived template (extension from name, Public: 0).
|
|
408
442
|
*/
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
443
|
+
const ROOT_FILE_TEMPLATES = {
|
|
444
|
+
'manifest.json': {
|
|
445
|
+
_entity: 'content',
|
|
446
|
+
_companionReferenceColumns: ['Content'],
|
|
447
|
+
Extension: 'JSON',
|
|
448
|
+
Public: 1,
|
|
449
|
+
Active: 1,
|
|
450
|
+
Title: 'PWA Manifest',
|
|
451
|
+
},
|
|
452
|
+
'CLAUDE.md': {
|
|
453
|
+
_entity: 'content',
|
|
454
|
+
_companionReferenceColumns: ['Content'],
|
|
455
|
+
Extension: 'MD',
|
|
456
|
+
Public: 0,
|
|
457
|
+
Active: 1,
|
|
458
|
+
Title: 'Claude Code Instructions',
|
|
459
|
+
},
|
|
460
|
+
'README.md': {
|
|
461
|
+
_entity: 'content',
|
|
462
|
+
_companionReferenceColumns: ['Content'],
|
|
463
|
+
Extension: 'MD',
|
|
464
|
+
Public: 1,
|
|
465
|
+
Active: 1,
|
|
466
|
+
Title: 'README',
|
|
467
|
+
},
|
|
468
|
+
};
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* For each filename in rootContentFiles, ensure a companion metadata file
|
|
472
|
+
* exists in lib/bins/app/. Skips if the file is absent at root or if metadata
|
|
473
|
+
* already tracks it. Creates metadata with Content: "@/<filename>" so push
|
|
474
|
+
* always reads from the project root.
|
|
475
|
+
*/
|
|
476
|
+
async function ensureRootContentFiles() {
|
|
477
|
+
const rootFiles = await loadRootContentFiles();
|
|
478
|
+
if (!rootFiles.length) return;
|
|
416
479
|
|
|
417
|
-
// Scan the entire project for any metadata file that already references manifest.json.
|
|
418
|
-
// This prevents creating duplicates when the metadata lives in an unexpected location.
|
|
419
|
-
// Check both @/manifest.json (root-relative) and @manifest.json (local) references,
|
|
420
|
-
// as well as Path: manifest.json which indicates a server record for this file.
|
|
421
480
|
const ig = await loadIgnore();
|
|
422
481
|
const allMeta = await findMetadataFiles(process.cwd(), ig);
|
|
482
|
+
|
|
483
|
+
// Build a set of already-tracked filenames from existing metadata (lowercase for case-insensitive match).
|
|
484
|
+
// Also strip stale fields (e.g. Descriptor) from content-entity root file metadata.
|
|
485
|
+
const tracked = new Set();
|
|
423
486
|
for (const metaPath of allMeta) {
|
|
424
487
|
try {
|
|
425
488
|
const raw = await readFile(metaPath, 'utf8');
|
|
426
489
|
const parsed = JSON.parse(raw);
|
|
427
|
-
|
|
428
|
-
|
|
490
|
+
const content = parsed.Content || '';
|
|
491
|
+
const path = parsed.Path || '';
|
|
492
|
+
// Detect both @/filename (root-relative) and @filename (local) references
|
|
493
|
+
const refName = content.startsWith('@/') ? content.slice(2)
|
|
494
|
+
: content.startsWith('@') ? content.slice(1)
|
|
495
|
+
: null;
|
|
496
|
+
if (refName) tracked.add(refName.toLowerCase());
|
|
497
|
+
if (path) tracked.add(path.replace(/^\//, '').toLowerCase());
|
|
498
|
+
|
|
499
|
+
// Clean up stale Descriptor field: content entities never have Descriptor.
|
|
500
|
+
// This fixes metadata written by an earlier buggy version of the tool.
|
|
501
|
+
if (parsed._entity === 'content' && parsed.Descriptor !== undefined) {
|
|
502
|
+
const cleaned = { ...parsed };
|
|
503
|
+
delete cleaned.Descriptor;
|
|
504
|
+
await writeFile(metaPath, JSON.stringify(cleaned, null, 2) + '\n');
|
|
505
|
+
log.dim(` Removed stale Descriptor field from ${basename(metaPath)}`);
|
|
506
|
+
}
|
|
429
507
|
} catch { /* skip unreadable */ }
|
|
430
508
|
}
|
|
431
509
|
|
|
432
|
-
// No existing metadata references manifest.json — create one
|
|
433
510
|
const appConfig = await loadAppConfig();
|
|
434
511
|
const structure = await loadStructureFile();
|
|
435
512
|
const appBin = findBinByPath('app', structure);
|
|
436
|
-
|
|
437
513
|
const binsAppDir = join(process.cwd(), BINS_DIR, 'app');
|
|
438
|
-
await mkdir(binsAppDir, { recursive: true });
|
|
439
514
|
|
|
440
|
-
const
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
515
|
+
for (const filename of rootFiles) {
|
|
516
|
+
// Skip if file doesn't exist at project root
|
|
517
|
+
try { await access(join(process.cwd(), filename)); } catch { continue; }
|
|
518
|
+
// Skip if already tracked by existing metadata (case-insensitive)
|
|
519
|
+
if (tracked.has(filename.toLowerCase())) continue;
|
|
520
|
+
|
|
521
|
+
const tmplKey = Object.keys(ROOT_FILE_TEMPLATES).find(k => k.toLowerCase() === filename.toLowerCase());
|
|
522
|
+
const tmpl = (tmplKey ? ROOT_FILE_TEMPLATES[tmplKey] : null) || {
|
|
523
|
+
_entity: 'content',
|
|
524
|
+
_companionReferenceColumns: ['Content'],
|
|
525
|
+
Extension: (filename.includes('.') ? filename.split('.').pop().toUpperCase() : 'TXT'),
|
|
526
|
+
Public: 0,
|
|
527
|
+
Active: 1,
|
|
528
|
+
Title: filename,
|
|
529
|
+
};
|
|
530
|
+
|
|
531
|
+
const meta = {
|
|
532
|
+
...tmpl,
|
|
533
|
+
Content: `@/${filename}`,
|
|
534
|
+
Path: filename,
|
|
535
|
+
Name: filename,
|
|
536
|
+
};
|
|
537
|
+
if (appBin) meta.BinID = appBin.binId;
|
|
538
|
+
if (appConfig.AppID) meta.AppID = appConfig.AppID;
|
|
539
|
+
|
|
540
|
+
await mkdir(binsAppDir, { recursive: true });
|
|
541
|
+
const stem = filename.replace(/\.[^.]+$/, '');
|
|
542
|
+
const metaFilename = `${stem}.metadata.json`;
|
|
543
|
+
await writeFile(join(binsAppDir, metaFilename), JSON.stringify(meta, null, 2) + '\n');
|
|
544
|
+
log.info(`Auto-created ${metaFilename} for ${filename}`);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
451
547
|
|
|
452
|
-
|
|
453
|
-
|
|
548
|
+
/**
|
|
549
|
+
* Collect all non-metadata file absolute paths from a directory (recursive).
|
|
550
|
+
*/
|
|
551
|
+
async function collectCompanionFiles(dirPath, ig) {
|
|
552
|
+
const results = [];
|
|
553
|
+
let entries;
|
|
554
|
+
try { entries = await readdir(dirPath, { withFileTypes: true }); } catch { return results; }
|
|
555
|
+
for (const entry of entries) {
|
|
556
|
+
const fullPath = join(process.cwd(), dirPath, entry.name);
|
|
557
|
+
if (entry.isDirectory()) {
|
|
558
|
+
const relPath = relative(process.cwd(), fullPath).replace(/\\/g, '/');
|
|
559
|
+
if (!ig.ignores(relPath + '/') && !ig.ignores(relPath)) {
|
|
560
|
+
results.push(...await collectCompanionFiles(join(dirPath, entry.name), ig));
|
|
561
|
+
}
|
|
562
|
+
} else if (!isMetadataFile(entry.name)) {
|
|
563
|
+
results.push(fullPath);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
return results;
|
|
567
|
+
}
|
|
454
568
|
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
569
|
+
/**
|
|
570
|
+
* Find metadata files project-wide that reference companion files in a given set via @reference.
|
|
571
|
+
* Used when pushing a directory like docs/ whose metadata lives elsewhere (e.g. lib/extension/Documentation/).
|
|
572
|
+
*/
|
|
573
|
+
async function findCrossDirectoryMetadata(dirPath, ig) {
|
|
574
|
+
const companionFiles = await collectCompanionFiles(dirPath, ig);
|
|
575
|
+
if (companionFiles.length === 0) return [];
|
|
576
|
+
|
|
577
|
+
const companionSet = new Set(companionFiles);
|
|
578
|
+
const appDepsPrefix = join(process.cwd(), 'app_dependencies') + sep;
|
|
579
|
+
const allMetaFiles = (await findMetadataFiles(process.cwd(), ig))
|
|
580
|
+
.filter(p => !p.startsWith(appDepsPrefix));
|
|
581
|
+
const found = [];
|
|
582
|
+
|
|
583
|
+
for (const metaPath of allMetaFiles) {
|
|
584
|
+
try {
|
|
585
|
+
const candidateMeta = JSON.parse(await readFile(metaPath, 'utf8'));
|
|
586
|
+
const metaDir = dirname(metaPath);
|
|
587
|
+
const cols = [...(candidateMeta._companionReferenceColumns || candidateMeta._contentColumns || [])];
|
|
588
|
+
if (candidateMeta._mediaFile) cols.push('_mediaFile');
|
|
589
|
+
for (const col of cols) {
|
|
590
|
+
const ref = candidateMeta[col];
|
|
591
|
+
if (!ref || !String(ref).startsWith('@')) continue;
|
|
592
|
+
const resolved = resolveAtReference(String(ref).substring(1), metaDir);
|
|
593
|
+
if (companionSet.has(resolved)) {
|
|
594
|
+
found.push(metaPath);
|
|
595
|
+
break;
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
} catch { /* skip unreadable */ }
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
return found;
|
|
458
602
|
}
|
|
459
603
|
|
|
460
604
|
/**
|
|
461
605
|
* Push all records found in a directory (recursive)
|
|
462
606
|
*/
|
|
463
607
|
async function pushDirectory(dirPath, client, options, modifyKey = null, transactionKey = 'RowUID', deletedCount = 0) {
|
|
464
|
-
// Auto-create
|
|
465
|
-
await
|
|
608
|
+
// Auto-create metadata for root content files (manifest.json, CLAUDE.md, README.md, etc.)
|
|
609
|
+
await ensureRootContentFiles();
|
|
466
610
|
|
|
467
611
|
// AUTO-ADD DISABLED: local-file-first approach is being replaced by server-first workflow.
|
|
468
612
|
// New records should be created via the DBO REST API first, then `dbo pull` to populate locally.
|
|
@@ -472,6 +616,14 @@ async function pushDirectory(dirPath, client, options, modifyKey = null, transac
|
|
|
472
616
|
const ig = await loadIgnore();
|
|
473
617
|
const metaFiles = await findMetadataFiles(dirPath, ig);
|
|
474
618
|
|
|
619
|
+
// Cross-directory metadata lookup: if the target dir has no metadata files of its own
|
|
620
|
+
// (e.g. docs/ only contains .md companions, with metadata in lib/extension/Documentation/),
|
|
621
|
+
// find metadata files project-wide that reference those companions via @reference.
|
|
622
|
+
if (metaFiles.length === 0) {
|
|
623
|
+
const crossMetas = await findCrossDirectoryMetadata(dirPath, ig);
|
|
624
|
+
metaFiles.push(...crossMetas);
|
|
625
|
+
}
|
|
626
|
+
|
|
475
627
|
// ── Load scripts config early (before delta detection) ──────────────
|
|
476
628
|
// Build hooks must run BEFORE delta detection so compiled output files
|
|
477
629
|
// (SASS → CSS, Rollup → JS, etc.) are on disk when changes are detected.
|
|
@@ -1353,24 +1505,19 @@ async function pushFromMetadata(meta, metaPath, client, options, changedColumns
|
|
|
1353
1505
|
// Clean up per-record ticket on success
|
|
1354
1506
|
await clearRecordTicket(uid || id);
|
|
1355
1507
|
|
|
1356
|
-
// Post-UID
|
|
1508
|
+
// Post-insert UID write: if the record lacked a UID and the server returned one
|
|
1357
1509
|
try {
|
|
1358
1510
|
const editResults2 = result.payload?.Results?.Edit || result.data?.Payload?.Results?.Edit || [];
|
|
1359
1511
|
const addResults2 = result.payload?.Results?.Add || result.data?.Payload?.Results?.Add || [];
|
|
1360
1512
|
const allResults = [...editResults2, ...addResults2];
|
|
1361
1513
|
if (allResults.length > 0 && !meta.UID) {
|
|
1362
1514
|
const serverUID = allResults[0].UID;
|
|
1363
|
-
if (serverUID
|
|
1515
|
+
if (serverUID) {
|
|
1364
1516
|
const config2 = await loadConfig();
|
|
1365
|
-
|
|
1366
|
-
if (renameResult.newMetaPath !== metaPath) {
|
|
1367
|
-
log.success(` Renamed to ~${serverUID} convention`);
|
|
1368
|
-
// Update metaPath reference for subsequent timestamp operations
|
|
1369
|
-
// (metaPath is const, but timestamp update below re-reads from meta)
|
|
1370
|
-
}
|
|
1517
|
+
await renameToUidConvention(meta, metaPath, serverUID, allResults[0]._LastUpdated, config2.ServerTimezone);
|
|
1371
1518
|
}
|
|
1372
1519
|
}
|
|
1373
|
-
} catch { /* non-critical
|
|
1520
|
+
} catch { /* non-critical */ }
|
|
1374
1521
|
|
|
1375
1522
|
// Update file timestamps from server response
|
|
1376
1523
|
try {
|
|
@@ -1378,12 +1525,14 @@ async function pushFromMetadata(meta, metaPath, client, options, changedColumns
|
|
|
1378
1525
|
if (editResults.length > 0) {
|
|
1379
1526
|
const updated = editResults[0]._LastUpdated || editResults[0].LastUpdated;
|
|
1380
1527
|
if (updated) {
|
|
1528
|
+
// Always update _LastUpdated in memory and on disk (required for baseline sync
|
|
1529
|
+
// even when ServerTimezone is not configured)
|
|
1530
|
+
meta._LastUpdated = updated;
|
|
1531
|
+
await writeFile(metaPath, JSON.stringify(meta, null, 2) + '\n');
|
|
1381
1532
|
const config = await loadConfig();
|
|
1382
1533
|
const serverTz = config.ServerTimezone;
|
|
1383
1534
|
if (serverTz) {
|
|
1384
|
-
//
|
|
1385
|
-
meta._LastUpdated = updated;
|
|
1386
|
-
await writeFile(metaPath, JSON.stringify(meta, null, 2) + '\n');
|
|
1535
|
+
// Set file mtimes to server timestamp (requires timezone for correct conversion)
|
|
1387
1536
|
await setFileTimestamps(metaPath, meta._CreatedOn, updated, serverTz);
|
|
1388
1537
|
// Update content file mtime too
|
|
1389
1538
|
const contentCols = meta._companionReferenceColumns || meta._contentColumns || [];
|
package/src/commands/status.js
CHANGED
|
@@ -59,16 +59,24 @@ export const statusCommand = new Command('status')
|
|
|
59
59
|
log.plain('');
|
|
60
60
|
log.info('Claude Code Plugins:');
|
|
61
61
|
for (const [name, scope] of Object.entries(scopes)) {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
62
|
+
let resolvedScope, location;
|
|
63
|
+
if (Array.isArray(scope)) {
|
|
64
|
+
// Registry format: [{ scope: 'user'|'project', installPath, ... }]
|
|
65
|
+
const entry = scope[0];
|
|
66
|
+
resolvedScope = entry.scope === 'user' ? 'global' : (entry.scope || 'project');
|
|
67
|
+
location = entry.installPath;
|
|
67
68
|
} else {
|
|
68
|
-
|
|
69
|
+
// Legacy format: scope is a plain string
|
|
70
|
+
resolvedScope = scope || 'project';
|
|
71
|
+
const fileName = `${name}.md`;
|
|
72
|
+
location = resolvedScope === 'global'
|
|
73
|
+
? join(homedir(), '.claude', 'commands', fileName)
|
|
74
|
+
: join(process.cwd(), '.claude', 'commands', fileName);
|
|
69
75
|
}
|
|
70
76
|
let installed = false;
|
|
71
|
-
|
|
77
|
+
if (location) {
|
|
78
|
+
try { await access(location); installed = true; } catch {}
|
|
79
|
+
}
|
|
72
80
|
const icon = installed ? '\u2713' : '\u2717';
|
|
73
81
|
const scopeLabel = resolvedScope === 'global' ? 'global' : 'project';
|
|
74
82
|
log.label(` ${name}`, `${icon} ${scopeLabel} (${installed ? location : 'not found'})`);
|
package/src/lib/config.js
CHANGED
|
@@ -173,6 +173,20 @@ export async function updateConfigWithApp({ AppID, AppUID, AppName, AppShortName
|
|
|
173
173
|
await writeFile(configPath(), JSON.stringify(existing, null, 2) + '\n');
|
|
174
174
|
}
|
|
175
175
|
|
|
176
|
+
/**
|
|
177
|
+
* Persist the UserMedia preference to .app/config.json.
|
|
178
|
+
* @param {boolean} value - true = download user media, false = skip
|
|
179
|
+
*/
|
|
180
|
+
export async function updateConfigUserMedia(value) {
|
|
181
|
+
await mkdir(projectDir(), { recursive: true });
|
|
182
|
+
let existing = {};
|
|
183
|
+
try {
|
|
184
|
+
existing = JSON.parse(await readFile(configPath(), 'utf8'));
|
|
185
|
+
} catch { /* no existing config */ }
|
|
186
|
+
existing.UserMedia = value;
|
|
187
|
+
await writeFile(configPath(), JSON.stringify(existing, null, 2) + '\n');
|
|
188
|
+
}
|
|
189
|
+
|
|
176
190
|
// ─── Dependency helpers ───────────────────────────────────────────────────
|
|
177
191
|
|
|
178
192
|
/**
|
|
@@ -513,7 +527,7 @@ export async function removeAppJsonReference(metaPath) {
|
|
|
513
527
|
// ─── config.local.json (global ~/.dbo/settings.json) ────────────────────
|
|
514
528
|
|
|
515
529
|
function configLocalPath() {
|
|
516
|
-
return join(homedir(), '.dbo', CONFIG_LOCAL_FILE);
|
|
530
|
+
return process.env.DBO_SETTINGS_PATH || join(homedir(), '.dbo', CONFIG_LOCAL_FILE);
|
|
517
531
|
}
|
|
518
532
|
|
|
519
533
|
async function ensureGlobalDboDir() {
|
|
@@ -803,8 +817,58 @@ export async function removeFromGitignore(pattern) {
|
|
|
803
817
|
log.dim(` Removed ${pattern} from .gitignore`);
|
|
804
818
|
}
|
|
805
819
|
|
|
820
|
+
/** Canonical set of gitignore entries for DBO CLI projects. */
|
|
821
|
+
export const DEFAULT_GITIGNORE_ENTRIES = [
|
|
822
|
+
'*local.',
|
|
823
|
+
'.DS_Store',
|
|
824
|
+
'*.DS_Store',
|
|
825
|
+
'*/.DS_Store',
|
|
826
|
+
'.idea*',
|
|
827
|
+
'.vscode*',
|
|
828
|
+
'*/node_modules',
|
|
829
|
+
'config.codekit3',
|
|
830
|
+
'/node_modules/',
|
|
831
|
+
'cookies.txt',
|
|
832
|
+
'*app.compiled.js',
|
|
833
|
+
'*.min.css',
|
|
834
|
+
'*.min.js',
|
|
835
|
+
'.profile',
|
|
836
|
+
'.secret',
|
|
837
|
+
'.password',
|
|
838
|
+
'.username',
|
|
839
|
+
'.cookies',
|
|
840
|
+
'.domain',
|
|
841
|
+
'.OverrideTicketID',
|
|
842
|
+
'media/*',
|
|
843
|
+
'',
|
|
844
|
+
'# DBO CLI (sensitive files)',
|
|
845
|
+
'.app/credentials.json',
|
|
846
|
+
'.app/cookies.txt',
|
|
847
|
+
'.app/ticketing.local.json',
|
|
848
|
+
'trash/',
|
|
849
|
+
'.app.json',
|
|
850
|
+
'',
|
|
851
|
+
'.app/scripts.local.json',
|
|
852
|
+
'.app/errors.log',
|
|
853
|
+
'',
|
|
854
|
+
'# DBO CLI (machine-generated baselines — regenerated by dbo pull/clone)',
|
|
855
|
+
'.app/operator.json',
|
|
856
|
+
'.app/operator.metadata.json',
|
|
857
|
+
'.app/synchronize.json',
|
|
858
|
+
'app_dependencies/',
|
|
859
|
+
'',
|
|
860
|
+
'# DBO CLI Claude Code commands (managed by dbo-cli)',
|
|
861
|
+
'.claude/plugins/dbo/',
|
|
862
|
+
'',
|
|
863
|
+
'# DBO CLI Claude Code commands (managed by dbo-cli)',
|
|
864
|
+
'.claude/plugins/track/',
|
|
865
|
+
];
|
|
866
|
+
|
|
806
867
|
/**
|
|
807
|
-
* Ensure
|
|
868
|
+
* Ensure entries are in .gitignore. Creates .gitignore if it doesn't exist.
|
|
869
|
+
* Accepts an array that may include comment lines and blank separators;
|
|
870
|
+
* only entries not already present in the file are appended, preserving
|
|
871
|
+
* the section structure (comments, blank lines) of the input list.
|
|
808
872
|
*/
|
|
809
873
|
export async function ensureGitignore(patterns) {
|
|
810
874
|
const gitignorePath = join(process.cwd(), '.gitignore');
|
|
@@ -813,18 +877,48 @@ export async function ensureGitignore(patterns) {
|
|
|
813
877
|
content = await readFile(gitignorePath, 'utf8');
|
|
814
878
|
} catch { /* no .gitignore yet */ }
|
|
815
879
|
|
|
816
|
-
const
|
|
817
|
-
if (
|
|
880
|
+
const isMissing = entry => entry.trim() !== '' && !content.includes(entry);
|
|
881
|
+
if (!patterns.some(isMissing)) return;
|
|
882
|
+
|
|
883
|
+
// Build the block to append: include missing entries with their surrounding
|
|
884
|
+
// structure (comments, blank separators), omitting blanks that end up adjacent
|
|
885
|
+
// to already-present entries.
|
|
886
|
+
const outputLines = [];
|
|
887
|
+
let prevWasContent = false;
|
|
888
|
+
|
|
889
|
+
for (const entry of patterns) {
|
|
890
|
+
if (entry.trim() === '') {
|
|
891
|
+
if (prevWasContent) outputLines.push('');
|
|
892
|
+
prevWasContent = false;
|
|
893
|
+
continue;
|
|
894
|
+
}
|
|
895
|
+
if (isMissing(entry)) {
|
|
896
|
+
outputLines.push(entry);
|
|
897
|
+
prevWasContent = true;
|
|
898
|
+
} else {
|
|
899
|
+
// Entry already exists — remove any optimistically-added trailing blank.
|
|
900
|
+
if (!prevWasContent && outputLines.length > 0 && outputLines[outputLines.length - 1].trim() === '') {
|
|
901
|
+
outputLines.pop();
|
|
902
|
+
}
|
|
903
|
+
prevWasContent = false;
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
// Strip trailing blank lines.
|
|
908
|
+
while (outputLines.length > 0 && outputLines[outputLines.length - 1].trim() === '') {
|
|
909
|
+
outputLines.pop();
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
if (outputLines.length === 0) return;
|
|
818
913
|
|
|
819
914
|
const isNew = content.length === 0;
|
|
820
915
|
const needsNewline = !isNew && !content.endsWith('\n');
|
|
821
|
-
const
|
|
822
|
-
const section = needsNewline ? '\n' : '';
|
|
823
|
-
const header = hasHeader ? '' : (isNew ? '# DBO CLI (sensitive files)\n' : '\n# DBO CLI (sensitive files)\n');
|
|
824
|
-
const addition = `${section}${header}${toAdd.join('\n')}\n`;
|
|
916
|
+
const prefix = needsNewline ? '\n\n' : (isNew ? '' : '\n');
|
|
825
917
|
|
|
826
|
-
await writeFile(gitignorePath, content +
|
|
827
|
-
|
|
918
|
+
await writeFile(gitignorePath, content + prefix + outputLines.join('\n') + '\n');
|
|
919
|
+
|
|
920
|
+
const added = outputLines.filter(l => l.trim() !== '' && !l.startsWith('#'));
|
|
921
|
+
for (const p of added) log.dim(` Added ${p} to .gitignore`);
|
|
828
922
|
}
|
|
829
923
|
|
|
830
924
|
// ─── Baseline (.app/<shortName>.json) ─────────────────────────────────────
|
|
@@ -1070,6 +1164,39 @@ export async function loadScriptsLocal() {
|
|
|
1070
1164
|
}
|
|
1071
1165
|
}
|
|
1072
1166
|
|
|
1167
|
+
// ─── Root Content Files ───────────────────────────────────────────────────────
|
|
1168
|
+
|
|
1169
|
+
const ROOT_CONTENT_FILES_DEFAULTS = ['CLAUDE.md', 'README.md', 'manifest.json', 'package.json', '.dboignore', '.gitignore'];
|
|
1170
|
+
|
|
1171
|
+
/**
|
|
1172
|
+
* Load rootContentFiles from .app/config.json.
|
|
1173
|
+
* If the key is absent, writes the defaults and returns them.
|
|
1174
|
+
* If the key is [], false, or null, returns [] (disabled).
|
|
1175
|
+
*/
|
|
1176
|
+
export async function loadRootContentFiles() {
|
|
1177
|
+
let existing = {};
|
|
1178
|
+
try { existing = JSON.parse(await readFile(configPath(), 'utf8')); } catch {}
|
|
1179
|
+
if (!('rootContentFiles' in existing)) {
|
|
1180
|
+
await saveRootContentFiles(ROOT_CONTENT_FILES_DEFAULTS);
|
|
1181
|
+
return ROOT_CONTENT_FILES_DEFAULTS;
|
|
1182
|
+
}
|
|
1183
|
+
const val = existing.rootContentFiles;
|
|
1184
|
+
if (!val || (Array.isArray(val) && val.length === 0)) return [];
|
|
1185
|
+
return Array.isArray(val) ? val : [];
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
/**
|
|
1189
|
+
* Persist rootContentFiles to .app/config.json.
|
|
1190
|
+
* Pass [] to disable root content file tracking.
|
|
1191
|
+
*/
|
|
1192
|
+
export async function saveRootContentFiles(list) {
|
|
1193
|
+
await mkdir(projectDir(), { recursive: true });
|
|
1194
|
+
let existing = {};
|
|
1195
|
+
try { existing = JSON.parse(await readFile(configPath(), 'utf8')); } catch {}
|
|
1196
|
+
existing.rootContentFiles = list;
|
|
1197
|
+
await writeFile(configPath(), JSON.stringify(existing, null, 2) + '\n');
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1073
1200
|
// ─── Tag Config ───────────────────────────────────────────────────────────────
|
|
1074
1201
|
|
|
1075
1202
|
/**
|