@dboio/cli 0.19.7 → 0.20.3
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 +18 -12
- package/bin/dbo.js +5 -0
- package/package.json +1 -1
- package/plugins/claude/dbo/.claude-plugin/plugin.json +1 -1
- package/plugins/claude/dbo/commands/dbo.md +39 -12
- package/plugins/claude/dbo/docs/dbo-cli-readme.md +18 -12
- package/plugins/claude/dbo/docs/dual-platform-maintenance.md +135 -0
- package/plugins/claude/dbo/skills/cookbook/SKILL.md +13 -3
- 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 +22 -19
- package/src/commands/clone.js +412 -57
- 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 +142 -43
- package/src/commands/status.js +15 -7
- package/src/lib/config.js +117 -11
- package/src/lib/dependencies.js +11 -9
- package/src/lib/filenames.js +54 -66
- package/src/lib/ignore.js +3 -0
- package/src/lib/input-parser.js +2 -6
- package/src/lib/insert.js +34 -49
- package/src/lib/structure.js +23 -8
- package/src/lib/ticketing.js +66 -9
- package/src/lib/toe-stepping.js +103 -3
- 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, loadRootContentFiles } from '../lib/config.js';
|
|
9
|
+
import { loadConfig, loadAppConfig, loadSynchronize, saveSynchronize, loadAppJsonBaseline, saveAppJsonBaseline, hasBaseline, loadScripts, loadScriptsLocal, addDeleteEntry, loadRootContentFiles, loadRepositoryIntegrationID } from '../lib/config.js';
|
|
10
10
|
import { mergeScriptsConfig, resolveHooks, buildHookEnv, runBuildLifecycle, runPushLifecycle } from '../lib/scripts.js';
|
|
11
|
-
import { checkStoredTicket, applyStoredTicketToSubmission, clearRecordTicket, clearGlobalTicket } from '../lib/ticketing.js';
|
|
11
|
+
import { checkStoredTicket, applyStoredTicketToSubmission, clearRecordTicket, clearGlobalTicket, fetchAndCacheRepositoryIntegration } 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';
|
|
@@ -380,8 +380,8 @@ async function pushSingleFile(filePath, client, options, modifyKey = null, trans
|
|
|
380
380
|
}
|
|
381
381
|
}
|
|
382
382
|
|
|
383
|
-
// Toe-stepping check for single-file push
|
|
384
|
-
if (isToeStepping(options) && meta.
|
|
383
|
+
// Toe-stepping check for single-file push (only for records confirmed on server)
|
|
384
|
+
if (isToeStepping(options) && (meta._CreatedOn || meta._LastUpdated)) {
|
|
385
385
|
const baseline = await loadAppJsonBaseline();
|
|
386
386
|
if (baseline) {
|
|
387
387
|
const appConfig = await loadAppConfig();
|
|
@@ -426,7 +426,13 @@ async function pushSingleFile(filePath, client, options, modifyKey = null, trans
|
|
|
426
426
|
}
|
|
427
427
|
// ── End script hooks ────────────────────────────────────────────────
|
|
428
428
|
|
|
429
|
-
const
|
|
429
|
+
const isNewRecord = !meta._CreatedOn && !meta._LastUpdated;
|
|
430
|
+
let success;
|
|
431
|
+
if (isNewRecord) {
|
|
432
|
+
success = await addFromMetadata(meta, metaPath, client, options, modifyKey);
|
|
433
|
+
} else {
|
|
434
|
+
success = await pushFromMetadata(meta, metaPath, client, options, null, modifyKey, transactionKey);
|
|
435
|
+
}
|
|
430
436
|
if (success) {
|
|
431
437
|
const baseline = await loadAppJsonBaseline();
|
|
432
438
|
if (baseline) {
|
|
@@ -480,7 +486,7 @@ async function ensureRootContentFiles() {
|
|
|
480
486
|
const ig = await loadIgnore();
|
|
481
487
|
const allMeta = await findMetadataFiles(process.cwd(), ig);
|
|
482
488
|
|
|
483
|
-
// Build a set of already-tracked filenames from existing metadata.
|
|
489
|
+
// Build a set of already-tracked filenames from existing metadata (lowercase for case-insensitive match).
|
|
484
490
|
// Also strip stale fields (e.g. Descriptor) from content-entity root file metadata.
|
|
485
491
|
const tracked = new Set();
|
|
486
492
|
for (const metaPath of allMeta) {
|
|
@@ -493,8 +499,8 @@ async function ensureRootContentFiles() {
|
|
|
493
499
|
const refName = content.startsWith('@/') ? content.slice(2)
|
|
494
500
|
: content.startsWith('@') ? content.slice(1)
|
|
495
501
|
: null;
|
|
496
|
-
if (refName) tracked.add(refName);
|
|
497
|
-
if (path) tracked.add(path.replace(/^\//, ''));
|
|
502
|
+
if (refName) tracked.add(refName.toLowerCase());
|
|
503
|
+
if (path) tracked.add(path.replace(/^\//, '').toLowerCase());
|
|
498
504
|
|
|
499
505
|
// Clean up stale Descriptor field: content entities never have Descriptor.
|
|
500
506
|
// This fixes metadata written by an earlier buggy version of the tool.
|
|
@@ -515,10 +521,11 @@ async function ensureRootContentFiles() {
|
|
|
515
521
|
for (const filename of rootFiles) {
|
|
516
522
|
// Skip if file doesn't exist at project root
|
|
517
523
|
try { await access(join(process.cwd(), filename)); } catch { continue; }
|
|
518
|
-
// Skip if already tracked by existing metadata
|
|
519
|
-
if (tracked.has(filename)) continue;
|
|
524
|
+
// Skip if already tracked by existing metadata (case-insensitive)
|
|
525
|
+
if (tracked.has(filename.toLowerCase())) continue;
|
|
520
526
|
|
|
521
|
-
const
|
|
527
|
+
const tmplKey = Object.keys(ROOT_FILE_TEMPLATES).find(k => k.toLowerCase() === filename.toLowerCase());
|
|
528
|
+
const tmpl = (tmplKey ? ROOT_FILE_TEMPLATES[tmplKey] : null) || {
|
|
522
529
|
_entity: 'content',
|
|
523
530
|
_companionReferenceColumns: ['Content'],
|
|
524
531
|
Extension: (filename.includes('.') ? filename.split('.').pop().toUpperCase() : 'TXT'),
|
|
@@ -544,6 +551,62 @@ async function ensureRootContentFiles() {
|
|
|
544
551
|
}
|
|
545
552
|
}
|
|
546
553
|
|
|
554
|
+
/**
|
|
555
|
+
* Collect all non-metadata file absolute paths from a directory (recursive).
|
|
556
|
+
*/
|
|
557
|
+
async function collectCompanionFiles(dirPath, ig) {
|
|
558
|
+
const results = [];
|
|
559
|
+
let entries;
|
|
560
|
+
try { entries = await readdir(dirPath, { withFileTypes: true }); } catch { return results; }
|
|
561
|
+
for (const entry of entries) {
|
|
562
|
+
const fullPath = join(process.cwd(), dirPath, entry.name);
|
|
563
|
+
if (entry.isDirectory()) {
|
|
564
|
+
const relPath = relative(process.cwd(), fullPath).replace(/\\/g, '/');
|
|
565
|
+
if (!ig.ignores(relPath + '/') && !ig.ignores(relPath)) {
|
|
566
|
+
results.push(...await collectCompanionFiles(join(dirPath, entry.name), ig));
|
|
567
|
+
}
|
|
568
|
+
} else if (!isMetadataFile(entry.name)) {
|
|
569
|
+
results.push(fullPath);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
return results;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/**
|
|
576
|
+
* Find metadata files project-wide that reference companion files in a given set via @reference.
|
|
577
|
+
* Used when pushing a directory like docs/ whose metadata lives elsewhere (e.g. lib/extension/Documentation/).
|
|
578
|
+
*/
|
|
579
|
+
async function findCrossDirectoryMetadata(dirPath, ig) {
|
|
580
|
+
const companionFiles = await collectCompanionFiles(dirPath, ig);
|
|
581
|
+
if (companionFiles.length === 0) return [];
|
|
582
|
+
|
|
583
|
+
const companionSet = new Set(companionFiles);
|
|
584
|
+
const appDepsPrefix = join(process.cwd(), 'app_dependencies') + sep;
|
|
585
|
+
const allMetaFiles = (await findMetadataFiles(process.cwd(), ig))
|
|
586
|
+
.filter(p => !p.startsWith(appDepsPrefix));
|
|
587
|
+
const found = [];
|
|
588
|
+
|
|
589
|
+
for (const metaPath of allMetaFiles) {
|
|
590
|
+
try {
|
|
591
|
+
const candidateMeta = JSON.parse(await readFile(metaPath, 'utf8'));
|
|
592
|
+
const metaDir = dirname(metaPath);
|
|
593
|
+
const cols = [...(candidateMeta._companionReferenceColumns || candidateMeta._contentColumns || [])];
|
|
594
|
+
if (candidateMeta._mediaFile) cols.push('_mediaFile');
|
|
595
|
+
for (const col of cols) {
|
|
596
|
+
const ref = candidateMeta[col];
|
|
597
|
+
if (!ref || !String(ref).startsWith('@')) continue;
|
|
598
|
+
const resolved = resolveAtReference(String(ref).substring(1), metaDir);
|
|
599
|
+
if (companionSet.has(resolved)) {
|
|
600
|
+
found.push(metaPath);
|
|
601
|
+
break;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
} catch { /* skip unreadable */ }
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
return found;
|
|
608
|
+
}
|
|
609
|
+
|
|
547
610
|
/**
|
|
548
611
|
* Push all records found in a directory (recursive)
|
|
549
612
|
*/
|
|
@@ -559,6 +622,14 @@ async function pushDirectory(dirPath, client, options, modifyKey = null, transac
|
|
|
559
622
|
const ig = await loadIgnore();
|
|
560
623
|
const metaFiles = await findMetadataFiles(dirPath, ig);
|
|
561
624
|
|
|
625
|
+
// Cross-directory metadata lookup: if the target dir has no metadata files of its own
|
|
626
|
+
// (e.g. docs/ only contains .md companions, with metadata in lib/extension/Documentation/),
|
|
627
|
+
// find metadata files project-wide that reference those companions via @reference.
|
|
628
|
+
if (metaFiles.length === 0) {
|
|
629
|
+
const crossMetas = await findCrossDirectoryMetadata(dirPath, ig);
|
|
630
|
+
metaFiles.push(...crossMetas);
|
|
631
|
+
}
|
|
632
|
+
|
|
562
633
|
// ── Load scripts config early (before delta detection) ──────────────
|
|
563
634
|
// Build hooks must run BEFORE delta detection so compiled output files
|
|
564
635
|
// (SASS → CSS, Rollup → JS, etc.) are on disk when changes are detected.
|
|
@@ -671,7 +742,7 @@ async function pushDirectory(dirPath, client, options, modifyKey = null, transac
|
|
|
671
742
|
continue;
|
|
672
743
|
}
|
|
673
744
|
|
|
674
|
-
const isNewRecord = !meta.
|
|
745
|
+
const isNewRecord = !meta._CreatedOn && !meta._LastUpdated;
|
|
675
746
|
|
|
676
747
|
// Verify @file references exist
|
|
677
748
|
const contentCols = meta._companionReferenceColumns || meta._contentColumns || [];
|
|
@@ -808,19 +879,50 @@ async function pushDirectory(dirPath, client, options, modifyKey = null, transac
|
|
|
808
879
|
// Pre-flight ticket validation (only if no --ticket flag)
|
|
809
880
|
const totalRecords = toPush.length + outputsWithChanges.length + binPushItems.length;
|
|
810
881
|
if (!options.ticket && totalRecords > 0) {
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
const
|
|
817
|
-
if (
|
|
818
|
-
|
|
819
|
-
|
|
882
|
+
// Proactive check: fetch RepositoryIntegrationID from the server before prompting.
|
|
883
|
+
// Uses UpdatedAfter=<today> to keep the response small; the top-level app record is always returned.
|
|
884
|
+
// Result is cached in .app/config.json so subsequent fetches can fall back to it.
|
|
885
|
+
let ticketingNeeded = null; // null = unknown (fetch failed)
|
|
886
|
+
const appConfig = await loadAppConfig();
|
|
887
|
+
const appShortNameForTicket = appConfig?.AppShortName;
|
|
888
|
+
if (appShortNameForTicket) {
|
|
889
|
+
const { id, fetched } = await fetchAndCacheRepositoryIntegration(client, appShortNameForTicket);
|
|
890
|
+
if (fetched) {
|
|
891
|
+
// Server answered: null means no RepositoryIntegration configured → skip ticketing
|
|
892
|
+
ticketingNeeded = (id != null);
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
// Fallback chain when the server fetch failed or no AppShortName:
|
|
897
|
+
// 1. Stored RepositoryIntegrationID in .app/config.json (from last successful fetch)
|
|
898
|
+
// 2. ticketing_required flag in ticketing.local.json (set reactively on first ticket_error)
|
|
899
|
+
if (ticketingNeeded === null) {
|
|
900
|
+
const storedId = await loadRepositoryIntegrationID();
|
|
901
|
+
if (storedId != null) {
|
|
902
|
+
ticketingNeeded = true;
|
|
903
|
+
}
|
|
904
|
+
// If storedId is also null, leave ticketingNeeded as null — checkStoredTicket will
|
|
905
|
+
// decide based on ticketing_required in ticketing.local.json (reactive fallback).
|
|
820
906
|
}
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
907
|
+
|
|
908
|
+
// Skip ticketing entirely when we have a confirmed negative signal
|
|
909
|
+
if (ticketingNeeded === false) {
|
|
910
|
+
// RepositoryIntegrationID is null on the server — no ticket needed
|
|
911
|
+
} else {
|
|
912
|
+
const recordSummary = [
|
|
913
|
+
...toPush.map(r => { const p = parseMetaFilename(basename(r.metaPath)); return p ? p.naturalBase : basename(r.metaPath, '.metadata.json'); }),
|
|
914
|
+
...outputsWithChanges.map(r => basename(r.metaPath, '.json')),
|
|
915
|
+
...binPushItems.map(r => `bin:${r.meta.Name}`),
|
|
916
|
+
].join(', ');
|
|
917
|
+
const ticketCheck = await checkStoredTicket(options, `${totalRecords} record(s): ${recordSummary}`);
|
|
918
|
+
if (ticketCheck.cancel) {
|
|
919
|
+
log.info('Submission cancelled');
|
|
920
|
+
return;
|
|
921
|
+
}
|
|
922
|
+
if (ticketCheck.clearTicket) {
|
|
923
|
+
await clearGlobalTicket();
|
|
924
|
+
log.dim(' Cleared stored ticket');
|
|
925
|
+
}
|
|
824
926
|
}
|
|
825
927
|
}
|
|
826
928
|
|
|
@@ -1116,8 +1218,10 @@ async function pushByUIDs(uids, client, options, modifyKey = null, transactionKe
|
|
|
1116
1218
|
}
|
|
1117
1219
|
|
|
1118
1220
|
/**
|
|
1119
|
-
* Submit a new record (
|
|
1120
|
-
* Builds RowID:add1 expressions
|
|
1221
|
+
* Submit a new record (insert) from metadata that has no _CreatedOn/_LastUpdated yet.
|
|
1222
|
+
* Builds RowID:add1 expressions and submits. A manually-specified UID (if present in
|
|
1223
|
+
* metadata, placed there by the developer) is included so the server uses that UID.
|
|
1224
|
+
* The server assigns _CreatedOn/_LastUpdated on success, which are written back.
|
|
1121
1225
|
*/
|
|
1122
1226
|
async function addFromMetadata(meta, metaPath, client, options, modifyKey = null) {
|
|
1123
1227
|
const entity = meta._entity;
|
|
@@ -1129,7 +1233,8 @@ async function addFromMetadata(meta, metaPath, client, options, modifyKey = null
|
|
|
1129
1233
|
|
|
1130
1234
|
for (const [key, value] of Object.entries(meta)) {
|
|
1131
1235
|
if (shouldSkipColumn(key)) continue;
|
|
1132
|
-
if
|
|
1236
|
+
// UID is included only if the developer manually placed it in the metadata file.
|
|
1237
|
+
// The CLI never auto-generates UIDs — the server assigns them on insert.
|
|
1133
1238
|
if (value === null || value === undefined) continue;
|
|
1134
1239
|
|
|
1135
1240
|
const strValue = String(value);
|
|
@@ -1201,7 +1306,7 @@ async function addFromMetadata(meta, metaPath, client, options, modifyKey = null
|
|
|
1201
1306
|
return false;
|
|
1202
1307
|
}
|
|
1203
1308
|
|
|
1204
|
-
// Extract UID from response
|
|
1309
|
+
// Extract UID and server-populated fields from response, write back to metadata
|
|
1205
1310
|
const addResults = result.payload?.Results?.Add || result.data?.Payload?.Results?.Add || [];
|
|
1206
1311
|
if (addResults.length > 0) {
|
|
1207
1312
|
const returnedUID = addResults[0].UID;
|
|
@@ -1221,7 +1326,7 @@ async function addFromMetadata(meta, metaPath, client, options, modifyKey = null
|
|
|
1221
1326
|
const config = await loadConfig();
|
|
1222
1327
|
const serverTz = config.ServerTimezone;
|
|
1223
1328
|
|
|
1224
|
-
//
|
|
1329
|
+
// Write UID and server timestamps back to metadata file; filenames are never renamed
|
|
1225
1330
|
const renameResult = await renameToUidConvention(meta, metaPath, returnedUID, returnedLastUpdated, serverTz);
|
|
1226
1331
|
|
|
1227
1332
|
// Propagate updated meta back (renameToUidConvention creates a new object)
|
|
@@ -1440,24 +1545,19 @@ async function pushFromMetadata(meta, metaPath, client, options, changedColumns
|
|
|
1440
1545
|
// Clean up per-record ticket on success
|
|
1441
1546
|
await clearRecordTicket(uid || id);
|
|
1442
1547
|
|
|
1443
|
-
// Post-UID
|
|
1548
|
+
// Post-push UID write: if the record lacked a UID (data record) and the server returned one
|
|
1444
1549
|
try {
|
|
1445
1550
|
const editResults2 = result.payload?.Results?.Edit || result.data?.Payload?.Results?.Edit || [];
|
|
1446
1551
|
const addResults2 = result.payload?.Results?.Add || result.data?.Payload?.Results?.Add || [];
|
|
1447
1552
|
const allResults = [...editResults2, ...addResults2];
|
|
1448
1553
|
if (allResults.length > 0 && !meta.UID) {
|
|
1449
1554
|
const serverUID = allResults[0].UID;
|
|
1450
|
-
if (serverUID
|
|
1555
|
+
if (serverUID) {
|
|
1451
1556
|
const config2 = await loadConfig();
|
|
1452
|
-
|
|
1453
|
-
if (renameResult.newMetaPath !== metaPath) {
|
|
1454
|
-
log.success(` Renamed to ~${serverUID} convention`);
|
|
1455
|
-
// Update metaPath reference for subsequent timestamp operations
|
|
1456
|
-
// (metaPath is const, but timestamp update below re-reads from meta)
|
|
1457
|
-
}
|
|
1557
|
+
await renameToUidConvention(meta, metaPath, serverUID, allResults[0]._LastUpdated, config2.ServerTimezone);
|
|
1458
1558
|
}
|
|
1459
1559
|
}
|
|
1460
|
-
} catch { /* non-critical
|
|
1560
|
+
} catch { /* non-critical */ }
|
|
1461
1561
|
|
|
1462
1562
|
// Update file timestamps from server response
|
|
1463
1563
|
try {
|
|
@@ -1578,8 +1678,7 @@ async function checkPathMismatch(meta, metaPath, entity, options) {
|
|
|
1578
1678
|
if (!contentFileName) return;
|
|
1579
1679
|
|
|
1580
1680
|
// Compute the current path based on where the file actually is.
|
|
1581
|
-
// Strip
|
|
1582
|
-
// server path and never contains the local ~UID suffix.
|
|
1681
|
+
// Strip any legacy ~UID suffix — the metadata Path is the canonical server path.
|
|
1583
1682
|
const uid = meta.UID;
|
|
1584
1683
|
const serverFileName = uid ? stripUidFromFilename(contentFileName, uid) : contentFileName;
|
|
1585
1684
|
const currentFilePath = join(metaDir, serverFileName);
|
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
|
@@ -527,7 +527,7 @@ export async function removeAppJsonReference(metaPath) {
|
|
|
527
527
|
// ─── config.local.json (global ~/.dbo/settings.json) ────────────────────
|
|
528
528
|
|
|
529
529
|
function configLocalPath() {
|
|
530
|
-
return join(homedir(), '.dbo', CONFIG_LOCAL_FILE);
|
|
530
|
+
return process.env.DBO_SETTINGS_PATH || join(homedir(), '.dbo', CONFIG_LOCAL_FILE);
|
|
531
531
|
}
|
|
532
532
|
|
|
533
533
|
async function ensureGlobalDboDir() {
|
|
@@ -794,6 +794,32 @@ export async function loadTicketSuggestionOutput() {
|
|
|
794
794
|
} catch { return null; }
|
|
795
795
|
}
|
|
796
796
|
|
|
797
|
+
/**
|
|
798
|
+
* Save RepositoryIntegrationID to .app/config.json.
|
|
799
|
+
* Stores the value fetched from the server's app object.
|
|
800
|
+
* Pass null to clear (ticketing not required for this app).
|
|
801
|
+
*/
|
|
802
|
+
export async function saveRepositoryIntegrationID(value) {
|
|
803
|
+
await mkdir(projectDir(), { recursive: true });
|
|
804
|
+
let existing = {};
|
|
805
|
+
try { existing = JSON.parse(await readFile(configPath(), 'utf8')); } catch {}
|
|
806
|
+
if (value != null) existing.RepositoryIntegrationID = value;
|
|
807
|
+
else delete existing.RepositoryIntegrationID;
|
|
808
|
+
await writeFile(configPath(), JSON.stringify(existing, null, 2) + '\n');
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
/**
|
|
812
|
+
* Load RepositoryIntegrationID from .app/config.json.
|
|
813
|
+
* Returns the stored value or null if not set.
|
|
814
|
+
*/
|
|
815
|
+
export async function loadRepositoryIntegrationID() {
|
|
816
|
+
try {
|
|
817
|
+
const raw = await readFile(configPath(), 'utf8');
|
|
818
|
+
const val = JSON.parse(raw).RepositoryIntegrationID;
|
|
819
|
+
return (val != null && val !== '') ? val : null;
|
|
820
|
+
} catch { return null; }
|
|
821
|
+
}
|
|
822
|
+
|
|
797
823
|
// ─── Gitignore ────────────────────────────────────────────────────────────
|
|
798
824
|
|
|
799
825
|
/**
|
|
@@ -817,8 +843,58 @@ export async function removeFromGitignore(pattern) {
|
|
|
817
843
|
log.dim(` Removed ${pattern} from .gitignore`);
|
|
818
844
|
}
|
|
819
845
|
|
|
846
|
+
/** Canonical set of gitignore entries for DBO CLI projects. */
|
|
847
|
+
export const DEFAULT_GITIGNORE_ENTRIES = [
|
|
848
|
+
'*local.',
|
|
849
|
+
'.DS_Store',
|
|
850
|
+
'*.DS_Store',
|
|
851
|
+
'*/.DS_Store',
|
|
852
|
+
'.idea*',
|
|
853
|
+
'.vscode*',
|
|
854
|
+
'*/node_modules',
|
|
855
|
+
'config.codekit3',
|
|
856
|
+
'/node_modules/',
|
|
857
|
+
'cookies.txt',
|
|
858
|
+
'*app.compiled.js',
|
|
859
|
+
'*.min.css',
|
|
860
|
+
'*.min.js',
|
|
861
|
+
'.profile',
|
|
862
|
+
'.secret',
|
|
863
|
+
'.password',
|
|
864
|
+
'.username',
|
|
865
|
+
'.cookies',
|
|
866
|
+
'.domain',
|
|
867
|
+
'.OverrideTicketID',
|
|
868
|
+
'media/*',
|
|
869
|
+
'',
|
|
870
|
+
'# DBO CLI (sensitive files)',
|
|
871
|
+
'.app/credentials.json',
|
|
872
|
+
'.app/cookies.txt',
|
|
873
|
+
'.app/ticketing.local.json',
|
|
874
|
+
'trash/',
|
|
875
|
+
'.app.json',
|
|
876
|
+
'',
|
|
877
|
+
'.app/scripts.local.json',
|
|
878
|
+
'.app/errors.log',
|
|
879
|
+
'',
|
|
880
|
+
'# DBO CLI (machine-generated baselines — regenerated by dbo pull/clone)',
|
|
881
|
+
'.app/operator.json',
|
|
882
|
+
'.app/operator.metadata.json',
|
|
883
|
+
'.app/synchronize.json',
|
|
884
|
+
'app_dependencies/',
|
|
885
|
+
'',
|
|
886
|
+
'# DBO CLI Claude Code commands (managed by dbo-cli)',
|
|
887
|
+
'.claude/plugins/dbo/',
|
|
888
|
+
'',
|
|
889
|
+
'# DBO CLI Claude Code commands (managed by dbo-cli)',
|
|
890
|
+
'.claude/plugins/track/',
|
|
891
|
+
];
|
|
892
|
+
|
|
820
893
|
/**
|
|
821
|
-
* Ensure
|
|
894
|
+
* Ensure entries are in .gitignore. Creates .gitignore if it doesn't exist.
|
|
895
|
+
* Accepts an array that may include comment lines and blank separators;
|
|
896
|
+
* only entries not already present in the file are appended, preserving
|
|
897
|
+
* the section structure (comments, blank lines) of the input list.
|
|
822
898
|
*/
|
|
823
899
|
export async function ensureGitignore(patterns) {
|
|
824
900
|
const gitignorePath = join(process.cwd(), '.gitignore');
|
|
@@ -827,18 +903,48 @@ export async function ensureGitignore(patterns) {
|
|
|
827
903
|
content = await readFile(gitignorePath, 'utf8');
|
|
828
904
|
} catch { /* no .gitignore yet */ }
|
|
829
905
|
|
|
830
|
-
const
|
|
831
|
-
if (
|
|
906
|
+
const isMissing = entry => entry.trim() !== '' && !content.includes(entry);
|
|
907
|
+
if (!patterns.some(isMissing)) return;
|
|
908
|
+
|
|
909
|
+
// Build the block to append: include missing entries with their surrounding
|
|
910
|
+
// structure (comments, blank separators), omitting blanks that end up adjacent
|
|
911
|
+
// to already-present entries.
|
|
912
|
+
const outputLines = [];
|
|
913
|
+
let prevWasContent = false;
|
|
914
|
+
|
|
915
|
+
for (const entry of patterns) {
|
|
916
|
+
if (entry.trim() === '') {
|
|
917
|
+
if (prevWasContent) outputLines.push('');
|
|
918
|
+
prevWasContent = false;
|
|
919
|
+
continue;
|
|
920
|
+
}
|
|
921
|
+
if (isMissing(entry)) {
|
|
922
|
+
outputLines.push(entry);
|
|
923
|
+
prevWasContent = true;
|
|
924
|
+
} else {
|
|
925
|
+
// Entry already exists — remove any optimistically-added trailing blank.
|
|
926
|
+
if (!prevWasContent && outputLines.length > 0 && outputLines[outputLines.length - 1].trim() === '') {
|
|
927
|
+
outputLines.pop();
|
|
928
|
+
}
|
|
929
|
+
prevWasContent = false;
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
// Strip trailing blank lines.
|
|
934
|
+
while (outputLines.length > 0 && outputLines[outputLines.length - 1].trim() === '') {
|
|
935
|
+
outputLines.pop();
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
if (outputLines.length === 0) return;
|
|
832
939
|
|
|
833
940
|
const isNew = content.length === 0;
|
|
834
941
|
const needsNewline = !isNew && !content.endsWith('\n');
|
|
835
|
-
const
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
const addition = `${section}${header}${toAdd.join('\n')}\n`;
|
|
942
|
+
const prefix = needsNewline ? '\n\n' : (isNew ? '' : '\n');
|
|
943
|
+
|
|
944
|
+
await writeFile(gitignorePath, content + prefix + outputLines.join('\n') + '\n');
|
|
839
945
|
|
|
840
|
-
|
|
841
|
-
for (const p of
|
|
946
|
+
const added = outputLines.filter(l => l.trim() !== '' && !l.startsWith('#'));
|
|
947
|
+
for (const p of added) log.dim(` Added ${p} to .gitignore`);
|
|
842
948
|
}
|
|
843
949
|
|
|
844
950
|
// ─── Baseline (.app/<shortName>.json) ─────────────────────────────────────
|
|
@@ -1086,7 +1192,7 @@ export async function loadScriptsLocal() {
|
|
|
1086
1192
|
|
|
1087
1193
|
// ─── Root Content Files ───────────────────────────────────────────────────────
|
|
1088
1194
|
|
|
1089
|
-
const ROOT_CONTENT_FILES_DEFAULTS = ['CLAUDE.md', 'README.md', 'manifest.json'];
|
|
1195
|
+
const ROOT_CONTENT_FILES_DEFAULTS = ['CLAUDE.md', 'README.md', 'manifest.json', 'package.json', '.dboignore', '.gitignore'];
|
|
1090
1196
|
|
|
1091
1197
|
/**
|
|
1092
1198
|
* Load rootContentFiles from .app/config.json.
|
package/src/lib/dependencies.js
CHANGED
|
@@ -289,15 +289,14 @@ export async function syncDependencies(options = {}) {
|
|
|
289
289
|
await symlinkCredentials(parentProjectDir, checkoutProjectDir);
|
|
290
290
|
|
|
291
291
|
// 3. Staleness check (unless --force or --schema)
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
checkoutEmpty = entries.every(e => e === '.app' || e.startsWith('.'));
|
|
299
|
-
} catch { /* dir doesn't exist yet — treat as empty */ }
|
|
292
|
+
// Track checkoutEmpty here so step 4 can decide whether to use the local schema.
|
|
293
|
+
let checkoutEmpty = true;
|
|
294
|
+
try {
|
|
295
|
+
const entries = await readdir(checkoutDir);
|
|
296
|
+
checkoutEmpty = entries.every(e => e === '.app' || e.startsWith('.'));
|
|
297
|
+
} catch { /* dir doesn't exist yet — treat as empty */ }
|
|
300
298
|
|
|
299
|
+
if (!forceAll) {
|
|
301
300
|
if (!checkoutEmpty) {
|
|
302
301
|
let isStale = true;
|
|
303
302
|
try {
|
|
@@ -314,7 +313,10 @@ export async function syncDependencies(options = {}) {
|
|
|
314
313
|
}
|
|
315
314
|
|
|
316
315
|
// 4. Run the clone (quiet — suppress child process output)
|
|
317
|
-
|
|
316
|
+
// Use the local schema file only for the very first (empty) checkout — it's a fast path
|
|
317
|
+
// for pre-bundled schemas. For stale checkouts always fetch from the server so that
|
|
318
|
+
// newly-added records (e.g. docs added after the schema was saved) are included.
|
|
319
|
+
if (shortname === '_system' && options.systemSchemaPath && checkoutEmpty) {
|
|
318
320
|
const relPath = relative(checkoutDir, options.systemSchemaPath);
|
|
319
321
|
await execFn(checkoutDir, ['clone', relPath, '--force', '--yes', '--no-deps'], { quiet: true });
|
|
320
322
|
} else {
|