@dboio/cli 0.13.2 → 0.15.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 +57 -0
- package/bin/dbo.js +2 -0
- package/package.json +1 -1
- package/plugins/claude/dbo/.claude-plugin/plugin.json +1 -1
- package/plugins/claude/dbo/commands/dbo.md +76 -74
- package/plugins/claude/dbo/docs/dbo-cli-readme.md +57 -0
- package/plugins/claude/dbo/skills/cli/SKILL.md +2 -1
- package/src/commands/add.js +12 -7
- package/src/commands/clone.js +138 -94
- package/src/commands/deploy.js +9 -2
- package/src/commands/diff.js +4 -4
- package/src/commands/login.js +30 -4
- package/src/commands/mv.js +17 -4
- package/src/commands/push.js +100 -103
- package/src/commands/rm.js +6 -4
- package/src/commands/tag.js +65 -0
- package/src/lib/config.js +28 -0
- package/src/lib/deploy-config.js +137 -0
- package/src/lib/diff.js +5 -4
- package/src/lib/filenames.js +89 -24
- package/src/lib/scaffold.js +1 -1
- package/src/lib/tagging.js +380 -0
- package/src/lib/toe-stepping.js +2 -1
- package/src/migrations/006-remove-uid-companion-filenames.js +3 -3
- package/src/migrations/007-natural-entity-companion-filenames.js +165 -0
- package/src/migrations/008-metadata-uid-in-suffix.js +70 -0
package/src/commands/login.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
|
-
import { loadConfig, saveUserInfo, saveUserProfile, ensureGitignore } from '../lib/config.js';
|
|
2
|
+
import { loadConfig, initConfig, saveUserInfo, saveUserProfile, ensureGitignore } from '../lib/config.js';
|
|
3
3
|
import { DboClient } from '../lib/client.js';
|
|
4
4
|
import { log } from '../lib/logger.js';
|
|
5
5
|
|
|
@@ -13,13 +13,24 @@ import { log } from '../lib/logger.js';
|
|
|
13
13
|
*/
|
|
14
14
|
export async function performLogin(domain, knownUsername) {
|
|
15
15
|
const config = await loadConfig();
|
|
16
|
+
const inquirer = (await import('inquirer')).default;
|
|
17
|
+
|
|
18
|
+
// Prompt for domain if not provided and not configured
|
|
19
|
+
if (!domain && !config.domain) {
|
|
20
|
+
const { domain: inputDomain } = await inquirer.prompt([
|
|
21
|
+
{ type: 'input', name: 'domain', message: 'Domain (e.g. myapp.dbo.io):' },
|
|
22
|
+
]);
|
|
23
|
+
if (!inputDomain) throw new Error('Domain is required.');
|
|
24
|
+
domain = inputDomain.trim();
|
|
25
|
+
await initConfig(domain);
|
|
26
|
+
}
|
|
27
|
+
|
|
16
28
|
const client = new DboClient({ domain });
|
|
17
29
|
|
|
18
30
|
let username = knownUsername || config.username;
|
|
19
31
|
let password;
|
|
20
32
|
|
|
21
33
|
// Interactive prompt for missing credentials
|
|
22
|
-
const inquirer = (await import('inquirer')).default;
|
|
23
34
|
const answers = await inquirer.prompt([
|
|
24
35
|
{ type: 'input', name: 'username', message: 'Username (email):', default: username || undefined, when: !username },
|
|
25
36
|
{ type: 'password', name: 'password', message: 'Password:', mask: '*' },
|
|
@@ -83,7 +94,23 @@ export const loginCommand = new Command('login')
|
|
|
83
94
|
.action(async (options) => {
|
|
84
95
|
try {
|
|
85
96
|
const config = await loadConfig();
|
|
86
|
-
const
|
|
97
|
+
const inquirer = (await import('inquirer')).default;
|
|
98
|
+
let domain = options.domain;
|
|
99
|
+
|
|
100
|
+
// Prompt for domain if not provided and not configured
|
|
101
|
+
if (!domain && !config.domain) {
|
|
102
|
+
const { domain: inputDomain } = await inquirer.prompt([
|
|
103
|
+
{ type: 'input', name: 'domain', message: 'Domain (e.g. myapp.dbo.io):' },
|
|
104
|
+
]);
|
|
105
|
+
if (!inputDomain) {
|
|
106
|
+
log.error('Domain is required.');
|
|
107
|
+
process.exit(1);
|
|
108
|
+
}
|
|
109
|
+
domain = inputDomain.trim();
|
|
110
|
+
await initConfig(domain);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const client = new DboClient({ domain });
|
|
87
114
|
|
|
88
115
|
let username = options.username || options.email || options.phone;
|
|
89
116
|
let password = options.password || options.passkey;
|
|
@@ -93,7 +120,6 @@ export const loginCommand = new Command('login')
|
|
|
93
120
|
|
|
94
121
|
// Interactive prompt if still missing
|
|
95
122
|
if (!username || !password) {
|
|
96
|
-
const inquirer = (await import('inquirer')).default;
|
|
97
123
|
const answers = await inquirer.prompt([
|
|
98
124
|
{ type: 'input', name: 'username', message: 'Username (email):', default: config.username, when: !username },
|
|
99
125
|
{ type: 'password', name: 'password', message: 'Password:', mask: '*', when: !password },
|
package/src/commands/mv.js
CHANGED
|
@@ -13,8 +13,9 @@ import {
|
|
|
13
13
|
BINS_DIR
|
|
14
14
|
} from '../lib/structure.js';
|
|
15
15
|
import { findMetadataFiles } from '../lib/diff.js';
|
|
16
|
-
import { findMetadataForCompanion } from '../lib/filenames.js';
|
|
16
|
+
import { isMetadataFile, parseMetaFilename, findMetadataForCompanion } from '../lib/filenames.js';
|
|
17
17
|
import { runPendingMigrations } from '../lib/migrations.js';
|
|
18
|
+
import { removeDeployEntry, upsertDeployEntry } from '../lib/deploy-config.js';
|
|
18
19
|
|
|
19
20
|
export const mvCommand = new Command('mv')
|
|
20
21
|
.description('Move files or bins to a new location and update metadata')
|
|
@@ -200,7 +201,7 @@ function checkCircularReference(sourceBinId, targetBinId, structure) {
|
|
|
200
201
|
* Resolve a file path to its metadata.json path.
|
|
201
202
|
*/
|
|
202
203
|
function resolveMetaPath(filePath) {
|
|
203
|
-
if (filePath.endsWith('.metadata.json')) {
|
|
204
|
+
if (isMetadataFile(basename(filePath)) || filePath.endsWith('.metadata.json')) {
|
|
204
205
|
return filePath;
|
|
205
206
|
}
|
|
206
207
|
const dir = dirname(filePath);
|
|
@@ -544,13 +545,13 @@ async function mvFile(sourceFile, targetBinId, structure, options) {
|
|
|
544
545
|
// Calculate paths
|
|
545
546
|
const targetDir = resolveBinPath(targetBinId, structure);
|
|
546
547
|
const sourceDir = dirname(metaPath);
|
|
547
|
-
const contentFileName = sourceFile.endsWith('.metadata.json')
|
|
548
|
+
const contentFileName = (isMetadataFile(basename(sourceFile)) || sourceFile.endsWith('.metadata.json'))
|
|
548
549
|
? null
|
|
549
550
|
: basename(sourceFile);
|
|
550
551
|
const metaFileName = basename(metaPath);
|
|
551
552
|
|
|
552
553
|
// Determine display name
|
|
553
|
-
const displayName = basename(metaPath, '.metadata.json');
|
|
554
|
+
const displayName = parseMetaFilename(basename(metaPath))?.naturalBase ?? basename(metaPath, '.metadata.json');
|
|
554
555
|
const targetBin = structure[targetBinId];
|
|
555
556
|
const targetBinName = targetBin ? targetBin.name : String(targetBinId);
|
|
556
557
|
const targetBinFullPath = targetBin ? `${BINS_DIR}/${targetBin.fullPath}` : targetDir;
|
|
@@ -672,6 +673,18 @@ async function mvFile(sourceFile, targetBinId, structure, options) {
|
|
|
672
673
|
}
|
|
673
674
|
}
|
|
674
675
|
|
|
676
|
+
if (!options.dryRun) {
|
|
677
|
+
// Update deploy config: remove old entry (by UID), re-insert with new path + correct key
|
|
678
|
+
await removeDeployEntry(uid);
|
|
679
|
+
if (newContentPath) {
|
|
680
|
+
const col = (meta._contentColumns || [])[0] || 'Content';
|
|
681
|
+
await upsertDeployEntry(newContentPath, uid, entity, col);
|
|
682
|
+
} else if (meta._mediaFile && String(meta._mediaFile).startsWith('@')) {
|
|
683
|
+
const movedMediaPath = join(targetDir, String(meta._mediaFile).substring(1));
|
|
684
|
+
await upsertDeployEntry(movedMediaPath, uid, entity, 'File');
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
|
|
675
688
|
if (options.dryRun) {
|
|
676
689
|
log.info(`[DRY RUN] Would move "${displayName}" to "${targetBinName}" (${targetBinFullPath})`);
|
|
677
690
|
} else {
|
package/src/commands/push.js
CHANGED
|
@@ -12,15 +12,25 @@ import { checkStoredTicket, applyStoredTicketToSubmission, clearRecordTicket, cl
|
|
|
12
12
|
import { checkModifyKey, isModifyKeyError, handleModifyKeyError } from '../lib/modify-key.js';
|
|
13
13
|
import { resolveTransactionKey } from '../lib/transaction-key.js';
|
|
14
14
|
import { setFileTimestamps } from '../lib/timestamps.js';
|
|
15
|
-
import { stripUidFromFilename, renameToUidConvention, hasUidInFilename, findMetadataForCompanion } from '../lib/filenames.js';
|
|
15
|
+
import { stripUidFromFilename, renameToUidConvention, hasUidInFilename, 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, getAllUserColumns, isReference, resolveReferencePath, detectBinChanges, synthesizeBinMetadata } from '../lib/delta.js';
|
|
19
19
|
import { BINS_DIR, ENTITY_DIR_NAMES, loadStructureFile, findBinByPath } from '../lib/structure.js';
|
|
20
|
-
import { ensureTrashIcon } from '../lib/
|
|
20
|
+
import { ensureTrashIcon, setFileTag } from '../lib/tagging.js';
|
|
21
21
|
import { checkToeStepping } from '../lib/toe-stepping.js';
|
|
22
22
|
import { runPendingMigrations } from '../lib/migrations.js';
|
|
23
|
-
import { findUnaddedFiles, detectBinFile, submitAdd } from './add.js';
|
|
23
|
+
// AUTO-ADD DISABLED: import { findUnaddedFiles, detectBinFile, submitAdd } from './add.js';
|
|
24
|
+
|
|
25
|
+
function _getMetaCompanionPaths(meta, metaPath) {
|
|
26
|
+
const dir = dirname(metaPath);
|
|
27
|
+
const paths = [];
|
|
28
|
+
for (const col of (meta._contentColumns || [])) {
|
|
29
|
+
const ref = meta[col];
|
|
30
|
+
if (ref && String(ref).startsWith('@')) paths.push(join(dir, String(ref).substring(1)));
|
|
31
|
+
}
|
|
32
|
+
return paths;
|
|
33
|
+
}
|
|
24
34
|
|
|
25
35
|
/**
|
|
26
36
|
* Resolve an @reference file path to an absolute filesystem path.
|
|
@@ -86,7 +96,7 @@ export const pushCommand = new Command('push')
|
|
|
86
96
|
const transactionKey = await resolveTransactionKey(options);
|
|
87
97
|
|
|
88
98
|
// Process pending deletions from synchronize.json
|
|
89
|
-
await processPendingDeletes(client, options, modifyKey, transactionKey);
|
|
99
|
+
const deletedCount = await processPendingDeletes(client, options, modifyKey, transactionKey) || 0;
|
|
90
100
|
|
|
91
101
|
// ── Resolution order ──────────────────────────────────────────
|
|
92
102
|
// 1. Commas → UID list
|
|
@@ -125,7 +135,7 @@ export const pushCommand = new Command('push')
|
|
|
125
135
|
log.dim(` Found: ${relative(process.cwd(), resolved)}`);
|
|
126
136
|
const resolvedStat = await stat(resolved);
|
|
127
137
|
if (resolvedStat.isDirectory()) {
|
|
128
|
-
await pushDirectory(resolved, client, options, modifyKey, transactionKey);
|
|
138
|
+
await pushDirectory(resolved, client, options, modifyKey, transactionKey, deletedCount);
|
|
129
139
|
} else {
|
|
130
140
|
await pushSingleFile(resolved, client, options, modifyKey, transactionKey);
|
|
131
141
|
}
|
|
@@ -146,7 +156,7 @@ export const pushCommand = new Command('push')
|
|
|
146
156
|
}
|
|
147
157
|
|
|
148
158
|
if (pathStat.isDirectory()) {
|
|
149
|
-
await pushDirectory(targetPath, client, options, modifyKey, transactionKey);
|
|
159
|
+
await pushDirectory(targetPath, client, options, modifyKey, transactionKey, deletedCount);
|
|
150
160
|
} else {
|
|
151
161
|
await pushSingleFile(targetPath, client, options, modifyKey, transactionKey);
|
|
152
162
|
}
|
|
@@ -161,18 +171,27 @@ export const pushCommand = new Command('push')
|
|
|
161
171
|
*/
|
|
162
172
|
async function processPendingDeletes(client, options, modifyKey = null, transactionKey = 'RowUID') {
|
|
163
173
|
const sync = await loadSynchronize();
|
|
164
|
-
if (!sync.delete || sync.delete.length === 0) return;
|
|
174
|
+
if (!sync.delete || sync.delete.length === 0) return 0;
|
|
165
175
|
|
|
166
176
|
log.info(`Processing ${sync.delete.length} pending deletion(s)...`);
|
|
167
177
|
|
|
168
178
|
const remaining = [];
|
|
169
179
|
const deletedUids = [];
|
|
170
180
|
|
|
181
|
+
// Load stored ticket once for all deletions (same as main push loop)
|
|
182
|
+
const { getGlobalTicket, getRecordTicket } = await import('../lib/ticketing.js');
|
|
183
|
+
const globalTicket = !options.ticket ? await getGlobalTicket() : null;
|
|
184
|
+
|
|
171
185
|
for (const entry of sync.delete) {
|
|
172
186
|
log.info(`Deleting "${entry.name}" (${entry.entity}:${entry.RowID})`);
|
|
173
187
|
|
|
174
188
|
const extraParams = { '_confirm': options.confirm || 'true' };
|
|
175
|
-
if (options.ticket)
|
|
189
|
+
if (options.ticket) {
|
|
190
|
+
extraParams['_OverrideTicketID'] = options.ticket;
|
|
191
|
+
} else {
|
|
192
|
+
const ticket = await getRecordTicket(entry.UID) || globalTicket;
|
|
193
|
+
if (ticket) extraParams['_OverrideTicketID'] = ticket;
|
|
194
|
+
}
|
|
176
195
|
if (modifyKey) extraParams['_modify_key'] = modifyKey;
|
|
177
196
|
const cachedUser2 = getSessionUserOverride();
|
|
178
197
|
if (cachedUser2) extraParams['_OverrideUserID'] = cachedUser2;
|
|
@@ -234,6 +253,8 @@ async function processPendingDeletes(client, options, modifyKey = null, transact
|
|
|
234
253
|
if (remaining.length > 0) {
|
|
235
254
|
log.warn(`${remaining.length} deletion(s) failed and remain staged.`);
|
|
236
255
|
}
|
|
256
|
+
|
|
257
|
+
return deletedUids.length;
|
|
237
258
|
}
|
|
238
259
|
|
|
239
260
|
/**
|
|
@@ -306,13 +327,20 @@ async function moveWillDeleteToTrash(entry) {
|
|
|
306
327
|
async function pushSingleFile(filePath, client, options, modifyKey = null, transactionKey = 'RowUID') {
|
|
307
328
|
// Find the metadata file
|
|
308
329
|
let metaPath;
|
|
309
|
-
if (filePath.endsWith('.metadata.json')) {
|
|
330
|
+
if (isMetadataFile(basename(filePath)) || filePath.endsWith('.metadata.json')) {
|
|
310
331
|
// User passed the metadata file directly — use it as-is
|
|
311
332
|
metaPath = filePath;
|
|
312
333
|
} else {
|
|
313
|
-
|
|
314
|
-
const
|
|
315
|
-
|
|
334
|
+
// Try findMetadataForCompanion first (handles both new and legacy formats)
|
|
335
|
+
const found = await findMetadataForCompanion(filePath);
|
|
336
|
+
if (found) {
|
|
337
|
+
metaPath = found;
|
|
338
|
+
} else {
|
|
339
|
+
// Fallback: old convention
|
|
340
|
+
const dir = dirname(filePath);
|
|
341
|
+
const base = basename(filePath, extname(filePath));
|
|
342
|
+
metaPath = join(dir, `${base}.metadata.json`);
|
|
343
|
+
}
|
|
316
344
|
}
|
|
317
345
|
|
|
318
346
|
let meta;
|
|
@@ -326,27 +354,12 @@ async function pushSingleFile(filePath, client, options, modifyKey = null, trans
|
|
|
326
354
|
try { meta = JSON.parse(await readFile(metaPath, 'utf8')); } catch {}
|
|
327
355
|
}
|
|
328
356
|
if (!meta) {
|
|
329
|
-
//
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
// After successful add, re-read the metadata (now has UID)
|
|
336
|
-
metaPath = binMeta.metaPath;
|
|
337
|
-
// The metadata file may have been renamed with ~UID, so scan for it
|
|
338
|
-
const updatedMeta = await findMetadataForCompanion(filePath);
|
|
339
|
-
if (updatedMeta) metaPath = updatedMeta;
|
|
340
|
-
meta = JSON.parse(await readFile(metaPath, 'utf8'));
|
|
341
|
-
log.info(`Successfully added — now pushing updates`);
|
|
342
|
-
} catch (err) {
|
|
343
|
-
log.error(`Auto-add failed for "${basename(filePath)}": ${err.message}`);
|
|
344
|
-
process.exit(1);
|
|
345
|
-
}
|
|
346
|
-
} else {
|
|
347
|
-
log.error(`No metadata found for "${basename(filePath)}". Pull the record first with "dbo pull".`);
|
|
348
|
-
process.exit(1);
|
|
349
|
-
}
|
|
357
|
+
// AUTO-ADD DISABLED: local-file-first approach is being replaced by server-first workflow.
|
|
358
|
+
// New records should be created via the DBO REST API first, then `dbo pull` to populate locally.
|
|
359
|
+
// The auto-add code (detectBinFile + submitAdd) has been commented out intentionally.
|
|
360
|
+
// See: .claude/2_specs/auto-deploy-config-generation.md — "server-first approach"
|
|
361
|
+
log.error(`No metadata found for "${basename(filePath)}". Create the record on the server first, then run "dbo pull".`);
|
|
362
|
+
process.exit(1);
|
|
350
363
|
}
|
|
351
364
|
}
|
|
352
365
|
|
|
@@ -456,61 +469,16 @@ async function ensureManifestMetadata() {
|
|
|
456
469
|
/**
|
|
457
470
|
* Push all records found in a directory (recursive)
|
|
458
471
|
*/
|
|
459
|
-
async function pushDirectory(dirPath, client, options, modifyKey = null, transactionKey = 'RowUID') {
|
|
472
|
+
async function pushDirectory(dirPath, client, options, modifyKey = null, transactionKey = 'RowUID', deletedCount = 0) {
|
|
460
473
|
// Auto-create manifest.metadata.json if manifest.json exists at root without companion metadata
|
|
461
474
|
await ensureManifestMetadata();
|
|
462
475
|
|
|
463
|
-
//
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
const unadded = await findUnaddedFiles(dirPath, ig);
|
|
468
|
-
if (unadded.length > 0) {
|
|
469
|
-
// Filter to files that detectBinFile can auto-classify (content/media in bins)
|
|
470
|
-
const autoAddable = [];
|
|
471
|
-
for (const filePath of unadded) {
|
|
472
|
-
const binMeta = await detectBinFile(filePath);
|
|
473
|
-
if (binMeta) autoAddable.push({ filePath, ...binMeta });
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
if (autoAddable.length > 0) {
|
|
477
|
-
log.info(`Found ${autoAddable.length} new file(s) to add before push:`);
|
|
478
|
-
for (const { filePath } of autoAddable) {
|
|
479
|
-
log.plain(` ${relative(process.cwd(), filePath)}`);
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
const doAdd = async () => {
|
|
483
|
-
for (const { meta, metaPath, filePath } of autoAddable) {
|
|
484
|
-
try {
|
|
485
|
-
await submitAdd(meta, metaPath, filePath, client, options);
|
|
486
|
-
// After submitAdd, meta.UID is set if successful
|
|
487
|
-
if (meta.UID) justAddedUIDs.add(meta.UID);
|
|
488
|
-
} catch (err) {
|
|
489
|
-
log.error(`Failed to add ${relative(process.cwd(), filePath)}: ${err.message}`);
|
|
490
|
-
}
|
|
491
|
-
}
|
|
492
|
-
};
|
|
493
|
-
|
|
494
|
-
if (!options.yes) {
|
|
495
|
-
const inquirer = (await import('inquirer')).default;
|
|
496
|
-
const { proceed } = await inquirer.prompt([{
|
|
497
|
-
type: 'confirm',
|
|
498
|
-
name: 'proceed',
|
|
499
|
-
message: `Add ${autoAddable.length} file(s) to the server before pushing?`,
|
|
500
|
-
default: true,
|
|
501
|
-
}]);
|
|
502
|
-
if (!proceed) {
|
|
503
|
-
log.dim('Skipping auto-add — continuing with push');
|
|
504
|
-
} else {
|
|
505
|
-
await doAdd();
|
|
506
|
-
}
|
|
507
|
-
} else {
|
|
508
|
-
await doAdd();
|
|
509
|
-
}
|
|
510
|
-
if (justAddedUIDs.size > 0) log.plain('');
|
|
511
|
-
}
|
|
512
|
-
}
|
|
476
|
+
// AUTO-ADD DISABLED: local-file-first approach is being replaced by server-first workflow.
|
|
477
|
+
// New records should be created via the DBO REST API first, then `dbo pull` to populate locally.
|
|
478
|
+
// The auto-add code (findUnaddedFiles + detectBinFile + submitAdd) has been commented out.
|
|
479
|
+
// See: .claude/2_specs/auto-deploy-config-generation.md — "server-first approach"
|
|
513
480
|
|
|
481
|
+
const ig = await loadIgnore();
|
|
514
482
|
const metaFiles = await findMetadataFiles(dirPath, ig);
|
|
515
483
|
|
|
516
484
|
// ── Load scripts config early (before delta detection) ──────────────
|
|
@@ -597,16 +565,16 @@ async function pushDirectory(dirPath, client, options, modifyKey = null, transac
|
|
|
597
565
|
continue;
|
|
598
566
|
}
|
|
599
567
|
|
|
600
|
-
//
|
|
601
|
-
if (meta.UID && justAddedUIDs.has(meta.UID)) {
|
|
602
|
-
log.dim(` Skipped (just added): ${basename(metaPath)}`);
|
|
603
|
-
continue;
|
|
604
|
-
}
|
|
568
|
+
// AUTO-ADD DISABLED: justAddedUIDs skip removed (server-first workflow)
|
|
605
569
|
|
|
606
|
-
//
|
|
607
|
-
//
|
|
608
|
-
|
|
609
|
-
|
|
570
|
+
// Output hierarchy entities: only push compound output files (root with inline children).
|
|
571
|
+
// Flat output metadata without .children are skipped — they are pushed as part of
|
|
572
|
+
// their parent compound file, or via `dbo deploy`. Never as standalone records.
|
|
573
|
+
if (meta._entity === 'output' || meta._entity === 'output_value'
|
|
574
|
+
|| meta._entity === 'output_value_filter' || meta._entity === 'output_value_entity_column_rel') {
|
|
575
|
+
if (meta._entity === 'output' && meta.children) {
|
|
576
|
+
outputCompoundFiles.push({ meta, metaPath });
|
|
577
|
+
}
|
|
610
578
|
continue;
|
|
611
579
|
}
|
|
612
580
|
|
|
@@ -702,23 +670,44 @@ async function pushDirectory(dirPath, client, options, modifyKey = null, transac
|
|
|
702
670
|
}
|
|
703
671
|
} catch { /* structure file missing or bin lookup failed — skip */ }
|
|
704
672
|
|
|
705
|
-
|
|
706
|
-
|
|
673
|
+
// Pre-filter compound output files: run delta detection early so unchanged outputs
|
|
674
|
+
// are excluded from the record count and ticket prompt (avoids false-positive prompts).
|
|
675
|
+
const outputsWithChanges = [];
|
|
676
|
+
for (const item of outputCompoundFiles) {
|
|
677
|
+
if (baseline) {
|
|
678
|
+
try {
|
|
679
|
+
const delta = await detectOutputChanges(item.metaPath, baseline);
|
|
680
|
+
const totalChanges = delta.root.length +
|
|
681
|
+
(delta.children ? Object.values(delta.children).reduce((s, c) => s + c.length, 0) : 999);
|
|
682
|
+
if (totalChanges === 0) {
|
|
683
|
+
log.dim(` Skipping ${basename(item.metaPath)} — no changes detected`);
|
|
684
|
+
skipped++;
|
|
685
|
+
continue;
|
|
686
|
+
}
|
|
687
|
+
} catch { /* delta detection failed — include in push for safety */ }
|
|
688
|
+
}
|
|
689
|
+
outputsWithChanges.push(item);
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
if (toPush.length === 0 && outputsWithChanges.length === 0 && binPushItems.length === 0) {
|
|
693
|
+
if (metaFiles.length === 0 && deletedCount === 0) {
|
|
707
694
|
log.warn(`No .metadata.json files found in "${dirPath}".`);
|
|
695
|
+
} else if (deletedCount > 0) {
|
|
696
|
+
log.info(`${deletedCount} deletion(s) processed. No other changes to push.`);
|
|
708
697
|
} else {
|
|
709
698
|
log.info('No changes to push');
|
|
710
699
|
}
|
|
711
700
|
return;
|
|
712
701
|
}
|
|
713
702
|
|
|
714
|
-
log.info(`Found ${
|
|
703
|
+
log.info(`Found ${toPush.length + outputsWithChanges.length + binPushItems.length} record(s) to push`);
|
|
715
704
|
|
|
716
705
|
// Pre-flight ticket validation (only if no --ticket flag)
|
|
717
|
-
const totalRecords = toPush.length +
|
|
706
|
+
const totalRecords = toPush.length + outputsWithChanges.length + binPushItems.length;
|
|
718
707
|
if (!options.ticket && totalRecords > 0) {
|
|
719
708
|
const recordSummary = [
|
|
720
|
-
...toPush.map(r => basename(r.metaPath, '.metadata.json')),
|
|
721
|
-
...
|
|
709
|
+
...toPush.map(r => { const p = parseMetaFilename(basename(r.metaPath)); return p ? p.naturalBase : basename(r.metaPath, '.metadata.json'); }),
|
|
710
|
+
...outputsWithChanges.map(r => basename(r.metaPath, '.json')),
|
|
722
711
|
...binPushItems.map(r => `bin:${r.meta.Name}`),
|
|
723
712
|
].join(', ');
|
|
724
713
|
const ticketCheck = await checkStoredTicket(options, `${totalRecords} record(s): ${recordSummary}`);
|
|
@@ -818,8 +807,8 @@ async function pushDirectory(dirPath, client, options, modifyKey = null, transac
|
|
|
818
807
|
}
|
|
819
808
|
}
|
|
820
809
|
|
|
821
|
-
// Process compound output files (root + inline children)
|
|
822
|
-
for (const { meta, metaPath } of
|
|
810
|
+
// Process compound output files (root + inline children) — already pre-filtered for changes
|
|
811
|
+
for (const { meta, metaPath } of outputsWithChanges) {
|
|
823
812
|
try {
|
|
824
813
|
const result = await pushOutputCompound(meta, metaPath, client, options, baseline, modifyKey, transactionKey);
|
|
825
814
|
if (result.pushed > 0) {
|
|
@@ -861,6 +850,13 @@ async function pushDirectory(dirPath, client, options, modifyKey = null, transac
|
|
|
861
850
|
await updateBaselineAfterPush(baseline, successfulPushes);
|
|
862
851
|
}
|
|
863
852
|
|
|
853
|
+
// Re-tag successfully pushed files as Synced (best-effort)
|
|
854
|
+
for (const { meta, metaPath } of successfulPushes) {
|
|
855
|
+
for (const filePath of _getMetaCompanionPaths(meta, metaPath)) {
|
|
856
|
+
setFileTag(filePath, 'synced').catch(() => {});
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
|
|
864
860
|
log.info(`Push complete: ${succeeded} succeeded, ${failed} failed, ${skipped} skipped`);
|
|
865
861
|
}
|
|
866
862
|
|
|
@@ -1221,7 +1217,8 @@ async function pushFromMetadata(meta, metaPath, client, options, changedColumns
|
|
|
1221
1217
|
}
|
|
1222
1218
|
|
|
1223
1219
|
const fieldLabel = changedColumns ? `${dataExprs.length} changed field(s)${isMediaUpload ? ' + media file' : ''}` : `${dataExprs.length} field(s)`;
|
|
1224
|
-
|
|
1220
|
+
const pushDisplayName = parseMetaFilename(basename(metaPath))?.naturalBase ?? basename(metaPath, '.metadata.json');
|
|
1221
|
+
log.info(`Pushing ${pushDisplayName} (${entity}:${rowKeyValue}) — ${fieldLabel}`);
|
|
1225
1222
|
|
|
1226
1223
|
// Apply stored ticket if no --ticket flag
|
|
1227
1224
|
const storedTicket = await applyStoredTicketToSubmission(dataExprs, entity, uid || id, uid || id, options);
|
|
@@ -1449,7 +1446,7 @@ async function checkPathMismatch(meta, metaPath, entity, options) {
|
|
|
1449
1446
|
if (ENTITY_DIR_NAMES.has(entity)) return;
|
|
1450
1447
|
|
|
1451
1448
|
const metaDir = dirname(metaPath);
|
|
1452
|
-
const metaBase = basename(metaPath, '.metadata.json');
|
|
1449
|
+
const metaBase = parseMetaFilename(basename(metaPath))?.naturalBase ?? basename(metaPath, '.metadata.json');
|
|
1453
1450
|
|
|
1454
1451
|
// Find the content file referenced by @filename
|
|
1455
1452
|
const contentCols = meta._contentColumns || [];
|
package/src/commands/rm.js
CHANGED
|
@@ -5,9 +5,10 @@ import { log } from '../lib/logger.js';
|
|
|
5
5
|
import { formatError } from '../lib/formatter.js';
|
|
6
6
|
import { addDeleteEntry, removeAppJsonReference } from '../lib/config.js';
|
|
7
7
|
import { findMetadataFiles } from '../lib/diff.js';
|
|
8
|
-
import { findMetadataForCompanion } from '../lib/filenames.js';
|
|
8
|
+
import { isMetadataFile, parseMetaFilename, findMetadataForCompanion } from '../lib/filenames.js';
|
|
9
9
|
import { loadStructureFile, findBinByPath, findChildBins, BINS_DIR } from '../lib/structure.js';
|
|
10
10
|
import { runPendingMigrations } from '../lib/migrations.js';
|
|
11
|
+
import { removeDeployEntry } from '../lib/deploy-config.js';
|
|
11
12
|
|
|
12
13
|
export const rmCommand = new Command('rm')
|
|
13
14
|
.description('Remove a file or directory locally and stage server deletions for the next dbo push')
|
|
@@ -42,7 +43,7 @@ export const rmCommand = new Command('rm')
|
|
|
42
43
|
* Resolve a file path to its metadata.json path.
|
|
43
44
|
*/
|
|
44
45
|
function resolveMetaPath(filePath) {
|
|
45
|
-
if (filePath.endsWith('.metadata.json')) {
|
|
46
|
+
if (isMetadataFile(basename(filePath)) || filePath.endsWith('.metadata.json')) {
|
|
46
47
|
return filePath;
|
|
47
48
|
}
|
|
48
49
|
const dir = dirname(filePath);
|
|
@@ -101,7 +102,7 @@ async function rmFileRecord(metaPath, options, { skipPrompt = false } = {}) {
|
|
|
101
102
|
localFiles.push(join(metaDir, String(meta._mediaFile).substring(1)));
|
|
102
103
|
}
|
|
103
104
|
|
|
104
|
-
const displayName = basename(metaPath, '.metadata.json');
|
|
105
|
+
const displayName = parseMetaFilename(basename(metaPath))?.naturalBase ?? basename(metaPath, '.metadata.json');
|
|
105
106
|
|
|
106
107
|
// Prompt if needed
|
|
107
108
|
if (!skipPrompt && !options.force) {
|
|
@@ -121,6 +122,7 @@ async function rmFileRecord(metaPath, options, { skipPrompt = false } = {}) {
|
|
|
121
122
|
// Stage deletion (include metaPath for Trash workflow in push.js)
|
|
122
123
|
const expression = `RowID:del${rowId};entity:${entity}=true`;
|
|
123
124
|
await addDeleteEntry({ UID: uid, RowID: rowId, entity, name: displayName, expression, metaPath });
|
|
125
|
+
await removeDeployEntry(uid);
|
|
124
126
|
log.success(` Staged: ${displayName} → ${expression}`);
|
|
125
127
|
|
|
126
128
|
// Remove from app.json
|
|
@@ -205,7 +207,7 @@ async function rmFile(filePath, options) {
|
|
|
205
207
|
localFiles.push(join(metaDir, String(meta._mediaFile).substring(1)));
|
|
206
208
|
}
|
|
207
209
|
|
|
208
|
-
const displayName = basename(metaPath, '.metadata.json');
|
|
210
|
+
const displayName = parseMetaFilename(basename(metaPath))?.naturalBase ?? basename(metaPath, '.metadata.json');
|
|
209
211
|
log.info(`Removing "${displayName}" (${entity}:${uid || rowId})`);
|
|
210
212
|
for (const f of localFiles) {
|
|
211
213
|
log.dim(` ${f}`);
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { tagProjectFiles } from '../lib/tagging.js';
|
|
4
|
+
import { loadTagConfig, saveTagConfig } from '../lib/config.js';
|
|
5
|
+
import { loadIgnore } from '../lib/ignore.js';
|
|
6
|
+
import { join } from 'path';
|
|
7
|
+
|
|
8
|
+
export const tagCommand = new Command('tag')
|
|
9
|
+
.description('Apply sync-status tags (Finder color tags / gio emblems) to project files')
|
|
10
|
+
.argument('[path]', 'File or directory to tag (defaults to entire project)')
|
|
11
|
+
.option('--clear', 'Remove all dbo:* tags from companion files')
|
|
12
|
+
.option('--status', 'Show counts of synced / modified / untracked / trashed files')
|
|
13
|
+
.option('--enable', 'Enable automatic tagging after clone/pull/push')
|
|
14
|
+
.option('--disable', 'Disable automatic tagging after clone/pull/push')
|
|
15
|
+
.option('--verbose', 'Log each file with its status')
|
|
16
|
+
.action(async (pathArg, options) => {
|
|
17
|
+
if (options.enable) {
|
|
18
|
+
await saveTagConfig(true);
|
|
19
|
+
console.log(chalk.green('✔ Automatic file tagging enabled'));
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
if (options.disable) {
|
|
23
|
+
await saveTagConfig(false);
|
|
24
|
+
console.log(chalk.yellow('Automatic file tagging disabled'));
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Verify project is initialized
|
|
29
|
+
const ig = await loadIgnore().catch(() => null);
|
|
30
|
+
if (!ig) {
|
|
31
|
+
console.error(chalk.red('Not a dbo project'));
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const dir = pathArg ? join(process.cwd(), pathArg) : process.cwd();
|
|
36
|
+
|
|
37
|
+
if (options.clear) {
|
|
38
|
+
await tagProjectFiles({ clearAll: true, dir, verbose: options.verbose });
|
|
39
|
+
console.log(chalk.green('✔ All dbo:* tags cleared'));
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const counts = await tagProjectFiles({ verbose: options.verbose, dir });
|
|
44
|
+
|
|
45
|
+
if (counts === null) {
|
|
46
|
+
const { tagFiles } = await loadTagConfig();
|
|
47
|
+
if (!tagFiles) {
|
|
48
|
+
console.log(chalk.yellow('File tagging is disabled. Run `dbo tag --enable` to enable.'));
|
|
49
|
+
} else {
|
|
50
|
+
console.log(chalk.dim('File tagging is not supported on this platform.'));
|
|
51
|
+
}
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (options.status || options.verbose) {
|
|
56
|
+
console.log(
|
|
57
|
+
`${chalk.green(counts.synced)} synced, ` +
|
|
58
|
+
`${chalk.blue(counts.modified)} modified, ` +
|
|
59
|
+
`${chalk.yellow(counts.untracked)} untracked, ` +
|
|
60
|
+
`${chalk.red(counts.trashed)} trashed`
|
|
61
|
+
);
|
|
62
|
+
} else {
|
|
63
|
+
console.log(chalk.green(`✔ Tags applied (${counts.synced} synced, ${counts.modified} modified, ${counts.untracked} untracked)`));
|
|
64
|
+
}
|
|
65
|
+
});
|
package/src/lib/config.js
CHANGED
|
@@ -936,3 +936,31 @@ export async function loadScriptsLocal() {
|
|
|
936
936
|
throw new SyntaxError(`Invalid JSON in .dbo/scripts.local.json: ${err.message}`);
|
|
937
937
|
}
|
|
938
938
|
}
|
|
939
|
+
|
|
940
|
+
// ─── Tag Config ───────────────────────────────────────────────────────────────
|
|
941
|
+
|
|
942
|
+
/**
|
|
943
|
+
* Load the tagFiles setting from config.json.
|
|
944
|
+
* @returns {Promise<{tagFiles: boolean}>} defaults to true
|
|
945
|
+
*/
|
|
946
|
+
export async function loadTagConfig() {
|
|
947
|
+
try {
|
|
948
|
+
const raw = await readFile(join(process.cwd(), DBO_DIR, CONFIG_FILE), 'utf8');
|
|
949
|
+
const config = JSON.parse(raw);
|
|
950
|
+
return { tagFiles: config.tagFiles !== false };
|
|
951
|
+
} catch {
|
|
952
|
+
return { tagFiles: true };
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
/**
|
|
957
|
+
* Enable or disable automatic file tagging by writing to config.json.
|
|
958
|
+
* @param {boolean} enabled
|
|
959
|
+
*/
|
|
960
|
+
export async function saveTagConfig(enabled) {
|
|
961
|
+
const configPath = join(process.cwd(), DBO_DIR, CONFIG_FILE);
|
|
962
|
+
let config = {};
|
|
963
|
+
try { config = JSON.parse(await readFile(configPath, 'utf8')); } catch {}
|
|
964
|
+
config.tagFiles = enabled;
|
|
965
|
+
await writeFile(configPath, JSON.stringify(config, null, 2));
|
|
966
|
+
}
|