@dboio/cli 0.11.4 → 0.15.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 +183 -3
- package/bin/dbo.js +6 -0
- package/package.json +1 -1
- package/plugins/claude/dbo/.claude-plugin/plugin.json +1 -1
- package/plugins/claude/dbo/commands/dbo.md +66 -243
- package/plugins/claude/dbo/docs/_audit_required/API/all.md +40 -0
- package/plugins/claude/dbo/docs/_audit_required/API/app.md +38 -0
- package/plugins/claude/dbo/docs/_audit_required/API/athenticate.md +26 -0
- package/plugins/claude/dbo/docs/_audit_required/API/cache.md +29 -0
- package/plugins/claude/dbo/docs/_audit_required/API/content.md +14 -0
- package/plugins/claude/dbo/docs/_audit_required/API/data_source.md +28 -0
- package/plugins/claude/dbo/docs/_audit_required/API/email.md +18 -0
- package/plugins/claude/dbo/docs/_audit_required/API/input.md +25 -0
- package/plugins/claude/dbo/docs/_audit_required/API/instance.md +28 -0
- package/plugins/claude/dbo/docs/_audit_required/API/log.md +8 -0
- package/plugins/claude/dbo/docs/_audit_required/API/media.md +12 -0
- package/plugins/claude/dbo/docs/_audit_required/API/output_by_entity.md +12 -0
- package/plugins/claude/dbo/docs/_audit_required/API/upload.md +7 -0
- package/plugins/claude/dbo/docs/_audit_required/dbo-api-syntax.md +1487 -0
- package/plugins/claude/dbo/docs/_audit_required/dbo-problems-code.md +111 -0
- package/plugins/claude/dbo/docs/_audit_required/dbo-problems-performance.md +109 -0
- package/plugins/claude/dbo/docs/_audit_required/dbo-problems-syntax.md +97 -0
- package/plugins/claude/dbo/docs/_audit_required/dbo-product-market.md +119 -0
- package/plugins/claude/dbo/docs/_audit_required/dbo-white-paper.md +125 -0
- package/plugins/claude/dbo/docs/dbo-cheat-sheet.md +323 -0
- package/plugins/claude/dbo/docs/dbo-cli-readme.md +2279 -0
- package/plugins/claude/dbo/docs/dbo-core-entities.md +878 -0
- package/plugins/claude/dbo/docs/dbo-output-customsql.md +677 -0
- package/plugins/claude/dbo/docs/dbo-output-query.md +967 -0
- package/plugins/claude/dbo/skills/cli/SKILL.md +63 -246
- package/src/commands/add.js +373 -64
- package/src/commands/build.js +102 -0
- package/src/commands/clone.js +719 -212
- package/src/commands/deploy.js +9 -2
- package/src/commands/diff.js +7 -3
- package/src/commands/init.js +16 -2
- package/src/commands/input.js +3 -1
- package/src/commands/login.js +30 -4
- package/src/commands/mv.js +28 -7
- package/src/commands/push.js +298 -78
- package/src/commands/rm.js +21 -6
- package/src/commands/run.js +81 -0
- package/src/commands/tag.js +65 -0
- package/src/lib/config.js +67 -0
- package/src/lib/delta.js +7 -1
- package/src/lib/deploy-config.js +137 -0
- package/src/lib/diff.js +28 -5
- package/src/lib/filenames.js +198 -54
- package/src/lib/ignore.js +6 -0
- package/src/lib/input-parser.js +13 -4
- package/src/lib/scaffold.js +1 -1
- package/src/lib/scripts.js +232 -0
- package/src/lib/tagging.js +380 -0
- package/src/lib/toe-stepping.js +2 -1
- package/src/migrations/006-remove-uid-companion-filenames.js +181 -0
- 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/push.js
CHANGED
|
@@ -6,19 +6,31 @@ import { buildInputBody, checkSubmitErrors, getSessionUserOverride } from '../li
|
|
|
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 } from '../lib/config.js';
|
|
9
|
+
import { loadConfig, loadAppConfig, loadSynchronize, saveSynchronize, loadAppJsonBaseline, saveAppJsonBaseline, hasBaseline, loadScripts, loadScriptsLocal } from '../lib/config.js';
|
|
10
|
+
import { mergeScriptsConfig, resolveHooks, buildHookEnv, runBuildLifecycle, runPushLifecycle } from '../lib/scripts.js';
|
|
10
11
|
import { checkStoredTicket, applyStoredTicketToSubmission, clearRecordTicket, clearGlobalTicket } from '../lib/ticketing.js';
|
|
11
12
|
import { checkModifyKey, isModifyKeyError, handleModifyKeyError } from '../lib/modify-key.js';
|
|
12
13
|
import { resolveTransactionKey } from '../lib/transaction-key.js';
|
|
13
14
|
import { setFileTimestamps } from '../lib/timestamps.js';
|
|
14
|
-
import { stripUidFromFilename, renameToUidConvention, hasUidInFilename,
|
|
15
|
+
import { stripUidFromFilename, renameToUidConvention, hasUidInFilename, isMetadataFile, parseMetaFilename, findMetadataForCompanion } from '../lib/filenames.js';
|
|
15
16
|
import { findMetadataFiles, findFileInProject, findByUID } from '../lib/diff.js';
|
|
16
17
|
import { loadIgnore } from '../lib/ignore.js';
|
|
17
18
|
import { detectChangedColumns, findBaselineEntry, detectOutputChanges, getAllUserColumns, isReference, resolveReferencePath, detectBinChanges, synthesizeBinMetadata } from '../lib/delta.js';
|
|
18
19
|
import { BINS_DIR, ENTITY_DIR_NAMES, loadStructureFile, findBinByPath } from '../lib/structure.js';
|
|
19
|
-
import { ensureTrashIcon } from '../lib/
|
|
20
|
+
import { ensureTrashIcon, setFileTag } from '../lib/tagging.js';
|
|
20
21
|
import { checkToeStepping } from '../lib/toe-stepping.js';
|
|
21
22
|
import { runPendingMigrations } from '../lib/migrations.js';
|
|
23
|
+
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
|
+
}
|
|
22
34
|
|
|
23
35
|
/**
|
|
24
36
|
* Resolve an @reference file path to an absolute filesystem path.
|
|
@@ -42,9 +54,16 @@ function isToeStepping(options) {
|
|
|
42
54
|
|
|
43
55
|
import { ENTITY_DEPENDENCIES } from '../lib/dependencies.js';
|
|
44
56
|
|
|
57
|
+
async function loadAndMergeScripts() {
|
|
58
|
+
const base = await loadScripts();
|
|
59
|
+
const local = await loadScriptsLocal();
|
|
60
|
+
if (!base && !local) return null;
|
|
61
|
+
return mergeScriptsConfig(base, local);
|
|
62
|
+
}
|
|
63
|
+
|
|
45
64
|
export const pushCommand = new Command('push')
|
|
46
65
|
.description('Push local files back to DBO.io using metadata from pull')
|
|
47
|
-
.argument('
|
|
66
|
+
.argument('[path]', 'File or directory to push (default: current directory)')
|
|
48
67
|
.option('-C, --confirm <value>', 'Commit: true (default) or false', 'true')
|
|
49
68
|
.option('--ticket <id>', 'Override ticket ID')
|
|
50
69
|
.option('--modify-key <key>', 'Provide ModifyKey directly (skips interactive prompt)')
|
|
@@ -58,7 +77,9 @@ export const pushCommand = new Command('push')
|
|
|
58
77
|
.option('-v, --verbose', 'Show HTTP request details')
|
|
59
78
|
.option('--domain <host>', 'Override domain')
|
|
60
79
|
.option('--no-migrate', 'Skip pending migrations for this invocation')
|
|
61
|
-
.
|
|
80
|
+
.option('--no-scripts', 'Bypass all script hooks; run default push pipeline unconditionally')
|
|
81
|
+
.option('--no-build', 'Skip the build phase (prebuild/build/postbuild); run push phase only')
|
|
82
|
+
.action(async (targetPath = ".", options) => {
|
|
62
83
|
try {
|
|
63
84
|
await runPendingMigrations(options);
|
|
64
85
|
const client = new DboClient({ domain: options.domain, verbose: options.verbose });
|
|
@@ -169,18 +190,29 @@ async function processPendingDeletes(client, options, modifyKey = null, transact
|
|
|
169
190
|
const body = await buildInputBody([entry.expression], extraParams);
|
|
170
191
|
|
|
171
192
|
try {
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
//
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
193
|
+
let result = await client.postUrlEncoded('/api/input/submit', body);
|
|
194
|
+
|
|
195
|
+
// Retry with prompted params if needed (ticket, repo mismatch, user)
|
|
196
|
+
if (!result.successful) {
|
|
197
|
+
const retryResult = await checkSubmitErrors(result, { rowUid: entry.UID });
|
|
198
|
+
if (retryResult) {
|
|
199
|
+
if (retryResult.skipRecord) { remaining.push(entry); continue; }
|
|
200
|
+
if (retryResult.skipAll) break;
|
|
201
|
+
if (retryResult.ticketExpressions?.length > 0) {
|
|
202
|
+
// Re-build body with ticket expressions added to the delete expression
|
|
203
|
+
const allExprs = [entry.expression, ...retryResult.ticketExpressions];
|
|
204
|
+
const retryParams = { ...extraParams, ...(retryResult.retryParams || retryResult) };
|
|
205
|
+
const retryBody = await buildInputBody(allExprs, retryParams);
|
|
206
|
+
result = await client.postUrlEncoded('/api/input/submit', retryBody);
|
|
207
|
+
} else {
|
|
208
|
+
const retryParams = { ...extraParams, ...(retryResult.retryParams || retryResult) };
|
|
209
|
+
const retryBody = await buildInputBody([entry.expression], retryParams);
|
|
210
|
+
result = await client.postUrlEncoded('/api/input/submit', retryBody);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (result.successful) {
|
|
184
216
|
log.success(` Deleted "${entry.name}" from server`);
|
|
185
217
|
deletedUids.push(entry.UID);
|
|
186
218
|
} else {
|
|
@@ -284,21 +316,55 @@ async function moveWillDeleteToTrash(entry) {
|
|
|
284
316
|
async function pushSingleFile(filePath, client, options, modifyKey = null, transactionKey = 'RowUID') {
|
|
285
317
|
// Find the metadata file
|
|
286
318
|
let metaPath;
|
|
287
|
-
if (filePath.endsWith('.metadata.json')) {
|
|
319
|
+
if (isMetadataFile(basename(filePath)) || filePath.endsWith('.metadata.json')) {
|
|
288
320
|
// User passed the metadata file directly — use it as-is
|
|
289
321
|
metaPath = filePath;
|
|
290
322
|
} else {
|
|
291
|
-
|
|
292
|
-
const
|
|
293
|
-
|
|
323
|
+
// Try findMetadataForCompanion first (handles both new and legacy formats)
|
|
324
|
+
const found = await findMetadataForCompanion(filePath);
|
|
325
|
+
if (found) {
|
|
326
|
+
metaPath = found;
|
|
327
|
+
} else {
|
|
328
|
+
// Fallback: old convention
|
|
329
|
+
const dir = dirname(filePath);
|
|
330
|
+
const base = basename(filePath, extname(filePath));
|
|
331
|
+
metaPath = join(dir, `${base}.metadata.json`);
|
|
332
|
+
}
|
|
294
333
|
}
|
|
295
334
|
|
|
296
335
|
let meta;
|
|
297
336
|
try {
|
|
298
337
|
meta = JSON.parse(await readFile(metaPath, 'utf8'));
|
|
299
338
|
} catch {
|
|
300
|
-
|
|
301
|
-
|
|
339
|
+
// Direct metadata path not found — search by @reference
|
|
340
|
+
const found = await findMetadataForCompanion(filePath);
|
|
341
|
+
if (found) {
|
|
342
|
+
metaPath = found;
|
|
343
|
+
try { meta = JSON.parse(await readFile(metaPath, 'utf8')); } catch {}
|
|
344
|
+
}
|
|
345
|
+
if (!meta) {
|
|
346
|
+
// Try auto-detecting as a bin content/media file and add it first
|
|
347
|
+
const binMeta = await detectBinFile(filePath);
|
|
348
|
+
if (binMeta) {
|
|
349
|
+
log.info(`No metadata found — auto-adding "${basename(filePath)}" first`);
|
|
350
|
+
try {
|
|
351
|
+
await submitAdd(binMeta.meta, binMeta.metaPath, filePath, client, options);
|
|
352
|
+
// After successful add, re-read the metadata (now has UID)
|
|
353
|
+
metaPath = binMeta.metaPath;
|
|
354
|
+
// The metadata file may have been renamed with ~UID, so scan for it
|
|
355
|
+
const updatedMeta = await findMetadataForCompanion(filePath);
|
|
356
|
+
if (updatedMeta) metaPath = updatedMeta;
|
|
357
|
+
meta = JSON.parse(await readFile(metaPath, 'utf8'));
|
|
358
|
+
log.info(`Successfully added — now pushing updates`);
|
|
359
|
+
} catch (err) {
|
|
360
|
+
log.error(`Auto-add failed for "${basename(filePath)}": ${err.message}`);
|
|
361
|
+
process.exit(1);
|
|
362
|
+
}
|
|
363
|
+
} else {
|
|
364
|
+
log.error(`No metadata found for "${basename(filePath)}". Pull the record first with "dbo pull".`);
|
|
365
|
+
process.exit(1);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
302
368
|
}
|
|
303
369
|
|
|
304
370
|
// Toe-stepping check for single-file push
|
|
@@ -311,6 +377,41 @@ async function pushSingleFile(filePath, client, options, modifyKey = null, trans
|
|
|
311
377
|
}
|
|
312
378
|
}
|
|
313
379
|
|
|
380
|
+
// ── Script hooks ────────────────────────────────────────────────────
|
|
381
|
+
if (options.scripts !== false) {
|
|
382
|
+
const scriptsConfig = await loadAndMergeScripts();
|
|
383
|
+
if (scriptsConfig) {
|
|
384
|
+
const relPath = relative(process.cwd(), filePath).replace(/\\/g, '/');
|
|
385
|
+
const entityType = meta._entity || '';
|
|
386
|
+
const hooks = resolveHooks(relPath, entityType, scriptsConfig);
|
|
387
|
+
const cfg = await loadConfig();
|
|
388
|
+
const app = await loadAppConfig();
|
|
389
|
+
const env = buildHookEnv(relPath, entityType, { ...app, domain: cfg.domain });
|
|
390
|
+
|
|
391
|
+
// Build phase
|
|
392
|
+
if (options.build !== false && (hooks.prebuild !== undefined || hooks.build !== undefined || hooks.postbuild !== undefined)) {
|
|
393
|
+
log.dim(` Running build hooks for ${basename(filePath)}...`);
|
|
394
|
+
const buildOk = await runBuildLifecycle(hooks, env, process.cwd());
|
|
395
|
+
if (!buildOk) {
|
|
396
|
+
log.error(`Build hook failed for "${basename(filePath)}" — aborting push`);
|
|
397
|
+
process.exit(1);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// Push phase
|
|
402
|
+
const pushResult = await runPushLifecycle(hooks, env, process.cwd());
|
|
403
|
+
if (pushResult.failed) {
|
|
404
|
+
log.error(`Push hook failed for "${basename(filePath)}" — aborting`);
|
|
405
|
+
process.exit(1);
|
|
406
|
+
}
|
|
407
|
+
if (!pushResult.runDefault) {
|
|
408
|
+
log.dim(` Skipped default push for "${basename(filePath)}" (custom push hook handled it)`);
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
// ── End script hooks ────────────────────────────────────────────────
|
|
414
|
+
|
|
314
415
|
await pushFromMetadata(meta, metaPath, client, options, null, modifyKey, transactionKey);
|
|
315
416
|
}
|
|
316
417
|
/**
|
|
@@ -328,13 +429,16 @@ async function ensureManifestMetadata() {
|
|
|
328
429
|
|
|
329
430
|
// Scan the entire project for any metadata file that already references manifest.json.
|
|
330
431
|
// This prevents creating duplicates when the metadata lives in an unexpected location.
|
|
432
|
+
// Check both @/manifest.json (root-relative) and @manifest.json (local) references,
|
|
433
|
+
// as well as Path: manifest.json which indicates a server record for this file.
|
|
331
434
|
const ig = await loadIgnore();
|
|
332
435
|
const allMeta = await findMetadataFiles(process.cwd(), ig);
|
|
333
436
|
for (const metaPath of allMeta) {
|
|
334
437
|
try {
|
|
335
438
|
const raw = await readFile(metaPath, 'utf8');
|
|
336
439
|
const parsed = JSON.parse(raw);
|
|
337
|
-
if (parsed.Content === '@/manifest.json') return;
|
|
440
|
+
if (parsed.Content === '@/manifest.json' || parsed.Content === '@manifest.json') return;
|
|
441
|
+
if (parsed.Path === 'manifest.json' || parsed.Path === '/manifest.json') return;
|
|
338
442
|
} catch { /* skip unreadable */ }
|
|
339
443
|
}
|
|
340
444
|
|
|
@@ -373,10 +477,116 @@ async function pushDirectory(dirPath, client, options, modifyKey = null, transac
|
|
|
373
477
|
// Auto-create manifest.metadata.json if manifest.json exists at root without companion metadata
|
|
374
478
|
await ensureManifestMetadata();
|
|
375
479
|
|
|
480
|
+
// ── Auto-add: detect un-added files and create+submit them before push ──
|
|
376
481
|
const ig = await loadIgnore();
|
|
482
|
+
const justAddedUIDs = new Set(); // Track UIDs added in this invocation — skip from push loop
|
|
483
|
+
|
|
484
|
+
const unadded = await findUnaddedFiles(dirPath, ig);
|
|
485
|
+
if (unadded.length > 0) {
|
|
486
|
+
// Filter to files that detectBinFile can auto-classify (content/media in bins)
|
|
487
|
+
const autoAddable = [];
|
|
488
|
+
for (const filePath of unadded) {
|
|
489
|
+
const binMeta = await detectBinFile(filePath);
|
|
490
|
+
if (binMeta) autoAddable.push({ filePath, ...binMeta });
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
if (autoAddable.length > 0) {
|
|
494
|
+
log.info(`Found ${autoAddable.length} new file(s) to add before push:`);
|
|
495
|
+
for (const { filePath } of autoAddable) {
|
|
496
|
+
log.plain(` ${relative(process.cwd(), filePath)}`);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
const doAdd = async () => {
|
|
500
|
+
for (const { meta, metaPath, filePath } of autoAddable) {
|
|
501
|
+
try {
|
|
502
|
+
await submitAdd(meta, metaPath, filePath, client, options);
|
|
503
|
+
// After submitAdd, meta.UID is set if successful
|
|
504
|
+
if (meta.UID) justAddedUIDs.add(meta.UID);
|
|
505
|
+
} catch (err) {
|
|
506
|
+
log.error(`Failed to add ${relative(process.cwd(), filePath)}: ${err.message}`);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
};
|
|
510
|
+
|
|
511
|
+
if (!options.yes) {
|
|
512
|
+
const inquirer = (await import('inquirer')).default;
|
|
513
|
+
const { proceed } = await inquirer.prompt([{
|
|
514
|
+
type: 'confirm',
|
|
515
|
+
name: 'proceed',
|
|
516
|
+
message: `Add ${autoAddable.length} file(s) to the server before pushing?`,
|
|
517
|
+
default: true,
|
|
518
|
+
}]);
|
|
519
|
+
if (!proceed) {
|
|
520
|
+
log.dim('Skipping auto-add — continuing with push');
|
|
521
|
+
} else {
|
|
522
|
+
await doAdd();
|
|
523
|
+
}
|
|
524
|
+
} else {
|
|
525
|
+
await doAdd();
|
|
526
|
+
}
|
|
527
|
+
if (justAddedUIDs.size > 0) log.plain('');
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
377
531
|
const metaFiles = await findMetadataFiles(dirPath, ig);
|
|
378
532
|
|
|
379
|
-
// Load
|
|
533
|
+
// ── Load scripts config early (before delta detection) ──────────────
|
|
534
|
+
// Build hooks must run BEFORE delta detection so compiled output files
|
|
535
|
+
// (SASS → CSS, Rollup → JS, etc.) are on disk when changes are detected.
|
|
536
|
+
let scriptsConfig = null;
|
|
537
|
+
let appConfigForHooks = null;
|
|
538
|
+
if (options.scripts !== false) {
|
|
539
|
+
scriptsConfig = await loadAndMergeScripts();
|
|
540
|
+
if (scriptsConfig) {
|
|
541
|
+
const cfg = await loadConfig();
|
|
542
|
+
const app = await loadAppConfig();
|
|
543
|
+
appConfigForHooks = { ...app, domain: cfg.domain };
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// ── Run build phase upfront (before delta detection) ────────────────
|
|
548
|
+
if (scriptsConfig && options.build !== false) {
|
|
549
|
+
const globalPrebuild = scriptsConfig.scripts?.prebuild;
|
|
550
|
+
const globalBuild = scriptsConfig.scripts?.build;
|
|
551
|
+
const globalPostbuild = scriptsConfig.scripts?.postbuild;
|
|
552
|
+
const globalHasAnyBuild = globalPrebuild !== undefined || globalBuild !== undefined || globalPostbuild !== undefined;
|
|
553
|
+
|
|
554
|
+
// 1. Run global build hooks once
|
|
555
|
+
if (globalHasAnyBuild) {
|
|
556
|
+
const globalHooks = { prebuild: globalPrebuild, build: globalBuild, postbuild: globalPostbuild };
|
|
557
|
+
const env = buildHookEnv('', '', appConfigForHooks);
|
|
558
|
+
log.dim(' Running global build hooks...');
|
|
559
|
+
const ok = await runBuildLifecycle(globalHooks, env, process.cwd());
|
|
560
|
+
if (!ok) {
|
|
561
|
+
log.error('Global build hook failed — aborting push');
|
|
562
|
+
process.exit(1);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// 2. Run per-target/entity build hooks for each metadata file
|
|
567
|
+
// (only when the resolved hook differs from the global — avoids re-running global)
|
|
568
|
+
for (const metaPath of metaFiles) {
|
|
569
|
+
let meta;
|
|
570
|
+
try { meta = JSON.parse(await readFile(metaPath, 'utf8')); } catch { continue; }
|
|
571
|
+
const relPath = relative(process.cwd(), metaPath.replace(/\.metadata\.json$/, '')).replace(/\\/g, '/');
|
|
572
|
+
const entityType = meta._entity || '';
|
|
573
|
+
const hooks = resolveHooks(relPath, entityType, scriptsConfig);
|
|
574
|
+
const hasNonGlobalBuild = (hooks.prebuild !== undefined && hooks.prebuild !== globalPrebuild)
|
|
575
|
+
|| (hooks.build !== undefined && hooks.build !== globalBuild)
|
|
576
|
+
|| (hooks.postbuild !== undefined && hooks.postbuild !== globalPostbuild);
|
|
577
|
+
if (hasNonGlobalBuild) {
|
|
578
|
+
const env = buildHookEnv(relPath, entityType, appConfigForHooks);
|
|
579
|
+
log.dim(` Build hooks: ${relPath}`);
|
|
580
|
+
const ok = await runBuildLifecycle(hooks, env, process.cwd());
|
|
581
|
+
if (!ok) {
|
|
582
|
+
log.error(`Build hook failed for "${basename(metaPath)}" — aborting push`);
|
|
583
|
+
process.exit(1);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// Load baseline for delta detection (after build hooks so compiled files are on disk)
|
|
380
590
|
const baseline = await loadAppJsonBaseline();
|
|
381
591
|
|
|
382
592
|
if (!baseline) {
|
|
@@ -404,6 +614,12 @@ async function pushDirectory(dirPath, client, options, modifyKey = null, transac
|
|
|
404
614
|
continue;
|
|
405
615
|
}
|
|
406
616
|
|
|
617
|
+
// Skip records that were just auto-added in this invocation — they're already on the server
|
|
618
|
+
if (meta.UID && justAddedUIDs.has(meta.UID)) {
|
|
619
|
+
log.dim(` Skipped (just added): ${basename(metaPath)}`);
|
|
620
|
+
continue;
|
|
621
|
+
}
|
|
622
|
+
|
|
407
623
|
// Compound output files: handle root + all inline children together
|
|
408
624
|
// These have _entity='output' and inline children under .children
|
|
409
625
|
if (meta._entity === 'output' && meta.children) {
|
|
@@ -518,7 +734,7 @@ async function pushDirectory(dirPath, client, options, modifyKey = null, transac
|
|
|
518
734
|
const totalRecords = toPush.length + outputCompoundFiles.length + binPushItems.length;
|
|
519
735
|
if (!options.ticket && totalRecords > 0) {
|
|
520
736
|
const recordSummary = [
|
|
521
|
-
...toPush.map(r => basename(r.metaPath, '.metadata.json')),
|
|
737
|
+
...toPush.map(r => { const p = parseMetaFilename(basename(r.metaPath)); return p ? p.naturalBase : basename(r.metaPath, '.metadata.json'); }),
|
|
522
738
|
...outputCompoundFiles.map(r => basename(r.metaPath, '.json')),
|
|
523
739
|
...binPushItems.map(r => `bin:${r.meta.Name}`),
|
|
524
740
|
].join(', ');
|
|
@@ -556,6 +772,19 @@ async function pushDirectory(dirPath, client, options, modifyKey = null, transac
|
|
|
556
772
|
}
|
|
557
773
|
for (const item of toAdd) {
|
|
558
774
|
try {
|
|
775
|
+
// Run push hooks for this item (build hooks already ran upfront)
|
|
776
|
+
if (scriptsConfig) {
|
|
777
|
+
const relPath = relative(process.cwd(), item.metaPath.replace(/\.metadata\.json$/, '')).replace(/\\/g, '/');
|
|
778
|
+
const entityType = item.meta._entity || '';
|
|
779
|
+
const hooks = resolveHooks(relPath, entityType, scriptsConfig);
|
|
780
|
+
if (hooks.prepush !== undefined || hooks.push !== undefined || hooks.postpush !== undefined) {
|
|
781
|
+
const env = buildHookEnv(relPath, entityType, appConfigForHooks);
|
|
782
|
+
const pushResult = await runPushLifecycle(hooks, env, process.cwd());
|
|
783
|
+
if (pushResult.failed) { log.error(`Push hook failed for "${basename(item.metaPath)}" — aborting`); failed++; break; }
|
|
784
|
+
if (!pushResult.runDefault) { succeeded++; successfulPushes.push(item); continue; }
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
|
|
559
788
|
const success = await addFromMetadata(item.meta, item.metaPath, client, options, modifyKey);
|
|
560
789
|
if (success) {
|
|
561
790
|
succeeded++;
|
|
@@ -576,6 +805,19 @@ async function pushDirectory(dirPath, client, options, modifyKey = null, transac
|
|
|
576
805
|
// Then process edits
|
|
577
806
|
for (const item of toEdit) {
|
|
578
807
|
try {
|
|
808
|
+
// Run push hooks for this item (build hooks already ran upfront)
|
|
809
|
+
if (scriptsConfig) {
|
|
810
|
+
const relPath = relative(process.cwd(), item.metaPath.replace(/\.metadata\.json$/, '')).replace(/\\/g, '/');
|
|
811
|
+
const entityType = item.meta._entity || '';
|
|
812
|
+
const hooks = resolveHooks(relPath, entityType, scriptsConfig);
|
|
813
|
+
if (hooks.prepush !== undefined || hooks.push !== undefined || hooks.postpush !== undefined) {
|
|
814
|
+
const env = buildHookEnv(relPath, entityType, appConfigForHooks);
|
|
815
|
+
const pushResult = await runPushLifecycle(hooks, env, process.cwd());
|
|
816
|
+
if (pushResult.failed) { log.error(`Push hook failed for "${basename(item.metaPath)}" — aborting`); failed++; break; }
|
|
817
|
+
if (!pushResult.runDefault) { succeeded++; successfulPushes.push(item); continue; }
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
|
|
579
821
|
const success = await pushFromMetadata(item.meta, item.metaPath, client, options, item.changedColumns, modifyKey, transactionKey);
|
|
580
822
|
if (success) {
|
|
581
823
|
succeeded++;
|
|
@@ -636,6 +878,13 @@ async function pushDirectory(dirPath, client, options, modifyKey = null, transac
|
|
|
636
878
|
await updateBaselineAfterPush(baseline, successfulPushes);
|
|
637
879
|
}
|
|
638
880
|
|
|
881
|
+
// Re-tag successfully pushed files as Synced (best-effort)
|
|
882
|
+
for (const { meta, metaPath } of successfulPushes) {
|
|
883
|
+
for (const filePath of _getMetaCompanionPaths(meta, metaPath)) {
|
|
884
|
+
setFileTag(filePath, 'synced').catch(() => {});
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
|
|
639
888
|
log.info(`Push complete: ${succeeded} succeeded, ${failed} failed, ${skipped} skipped`);
|
|
640
889
|
}
|
|
641
890
|
|
|
@@ -866,66 +1115,31 @@ async function addFromMetadata(meta, metaPath, client, options, modifyKey = null
|
|
|
866
1115
|
return false;
|
|
867
1116
|
}
|
|
868
1117
|
|
|
869
|
-
// Extract UID from response and rename
|
|
1118
|
+
// Extract UID from response and rename metadata to ~uid convention
|
|
870
1119
|
const addResults = result.payload?.Results?.Add || result.data?.Payload?.Results?.Add || [];
|
|
871
1120
|
if (addResults.length > 0) {
|
|
872
1121
|
const returnedUID = addResults[0].UID;
|
|
873
1122
|
const returnedLastUpdated = addResults[0]._LastUpdated;
|
|
874
1123
|
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
const entityIdKey = entity.charAt(0).toUpperCase() + entity.slice(1) + 'ID';
|
|
880
|
-
const returnedId = addResults[0][entityIdKey] || addResults[0]._id || addResults[0].ID;
|
|
881
|
-
if (returnedId) meta._id = returnedId;
|
|
882
|
-
|
|
883
|
-
const currentMetaBase = basename(metaPath, '.metadata.json');
|
|
1124
|
+
// Store numeric ID for delete operations (RowID:del<id>)
|
|
1125
|
+
const entityIdKey = entity.charAt(0).toUpperCase() + entity.slice(1) + 'ID';
|
|
1126
|
+
const returnedId = addResults[0][entityIdKey] || addResults[0]._id || addResults[0].ID;
|
|
1127
|
+
if (returnedId) meta._id = returnedId;
|
|
884
1128
|
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
await writeFile(metaPath, JSON.stringify(meta, null, 2) + '\n');
|
|
888
|
-
log.success(`UID ${returnedUID} already in filename`);
|
|
889
|
-
return true;
|
|
890
|
-
}
|
|
891
|
-
|
|
892
|
-
const newBase = buildUidFilename(currentMetaBase, returnedUID);
|
|
893
|
-
const newMetaPath = join(metaDir, `${newBase}.metadata.json`);
|
|
894
|
-
|
|
895
|
-
// Update @references in metadata to include ~UID for non-root references
|
|
896
|
-
for (const col of (meta._contentColumns || [])) {
|
|
897
|
-
const ref = meta[col];
|
|
898
|
-
if (ref && String(ref).startsWith('@') && !String(ref).startsWith('@/')) {
|
|
899
|
-
// Local file reference — rename it too
|
|
900
|
-
const oldRefFile = String(ref).substring(1);
|
|
901
|
-
const refExt = extname(oldRefFile);
|
|
902
|
-
const refBase = basename(oldRefFile, refExt);
|
|
903
|
-
const newRefBase = buildUidFilename(refBase, returnedUID);
|
|
904
|
-
const newRefFile = refExt ? `${newRefBase}${refExt}` : newRefBase;
|
|
905
|
-
|
|
906
|
-
const oldRefPath = join(metaDir, oldRefFile);
|
|
907
|
-
const newRefPath = join(metaDir, newRefFile);
|
|
908
|
-
try {
|
|
909
|
-
await fsRename(oldRefPath, newRefPath);
|
|
910
|
-
meta[col] = `@${newRefFile}`;
|
|
911
|
-
} catch { /* content file may be root-relative */ }
|
|
912
|
-
}
|
|
913
|
-
}
|
|
1129
|
+
// Write _LastUpdated back to meta so baseline gets it
|
|
1130
|
+
if (returnedLastUpdated) meta._LastUpdated = returnedLastUpdated;
|
|
914
1131
|
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
try { await fsRename(metaPath, newMetaPath); } catch { /* ignore if same */ }
|
|
918
|
-
}
|
|
919
|
-
await writeFile(newMetaPath, JSON.stringify(meta, null, 2) + '\n');
|
|
1132
|
+
if (returnedUID) {
|
|
1133
|
+
meta.UID = returnedUID;
|
|
920
1134
|
|
|
921
|
-
// Set timestamps from server
|
|
922
1135
|
const config = await loadConfig();
|
|
923
1136
|
const serverTz = config.ServerTimezone;
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
1137
|
+
|
|
1138
|
+
// Rename metadata file to ~UID convention; companions keep natural names
|
|
1139
|
+
const renameResult = await renameToUidConvention(meta, metaPath, returnedUID, returnedLastUpdated, serverTz);
|
|
1140
|
+
|
|
1141
|
+
// Propagate updated meta back (renameToUidConvention creates a new object)
|
|
1142
|
+
Object.assign(meta, renameResult.updatedMeta);
|
|
929
1143
|
|
|
930
1144
|
log.success(`Added: ${basename(metaPath)} → UID ${returnedUID}`);
|
|
931
1145
|
}
|
|
@@ -1031,7 +1245,8 @@ async function pushFromMetadata(meta, metaPath, client, options, changedColumns
|
|
|
1031
1245
|
}
|
|
1032
1246
|
|
|
1033
1247
|
const fieldLabel = changedColumns ? `${dataExprs.length} changed field(s)${isMediaUpload ? ' + media file' : ''}` : `${dataExprs.length} field(s)`;
|
|
1034
|
-
|
|
1248
|
+
const pushDisplayName = parseMetaFilename(basename(metaPath))?.naturalBase ?? basename(metaPath, '.metadata.json');
|
|
1249
|
+
log.info(`Pushing ${pushDisplayName} (${entity}:${rowKeyValue}) — ${fieldLabel}`);
|
|
1035
1250
|
|
|
1036
1251
|
// Apply stored ticket if no --ticket flag
|
|
1037
1252
|
const storedTicket = await applyStoredTicketToSubmission(dataExprs, entity, uid || id, uid || id, options);
|
|
@@ -1259,7 +1474,7 @@ async function checkPathMismatch(meta, metaPath, entity, options) {
|
|
|
1259
1474
|
if (ENTITY_DIR_NAMES.has(entity)) return;
|
|
1260
1475
|
|
|
1261
1476
|
const metaDir = dirname(metaPath);
|
|
1262
|
-
const metaBase = basename(metaPath, '.metadata.json');
|
|
1477
|
+
const metaBase = parseMetaFilename(basename(metaPath))?.naturalBase ?? basename(metaPath, '.metadata.json');
|
|
1263
1478
|
|
|
1264
1479
|
// Find the content file referenced by @filename
|
|
1265
1480
|
const contentCols = meta._contentColumns || [];
|
|
@@ -1469,6 +1684,11 @@ async function _submitOutputEntity(entity, physicalEntity, changedColumns, metaD
|
|
|
1469
1684
|
const strValue = (val === null || val === undefined) ? '' : String(val);
|
|
1470
1685
|
if (isReference(strValue)) {
|
|
1471
1686
|
const refPath = resolveReferencePath(strValue, metaDir);
|
|
1687
|
+
// Skip missing companion files (e.g. empty CustomSQL not extracted by clone)
|
|
1688
|
+
try { await access(refPath); } catch {
|
|
1689
|
+
log.dim(` Skipping ${col} — companion file not found: ${basename(refPath)}`);
|
|
1690
|
+
continue;
|
|
1691
|
+
}
|
|
1472
1692
|
dataExprs.push(`${rowKeyPrefix}:${rowKeyValue};column:${physicalEntity}.${col}@${refPath}`);
|
|
1473
1693
|
} else {
|
|
1474
1694
|
dataExprs.push(`${rowKeyPrefix}:${rowKeyValue};column:${physicalEntity}.${col}=${strValue}`);
|
package/src/commands/rm.js
CHANGED
|
@@ -5,8 +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 { isMetadataFile, parseMetaFilename, findMetadataForCompanion } from '../lib/filenames.js';
|
|
8
9
|
import { loadStructureFile, findBinByPath, findChildBins, BINS_DIR } from '../lib/structure.js';
|
|
9
10
|
import { runPendingMigrations } from '../lib/migrations.js';
|
|
11
|
+
import { removeDeployEntry } from '../lib/deploy-config.js';
|
|
10
12
|
|
|
11
13
|
export const rmCommand = new Command('rm')
|
|
12
14
|
.description('Remove a file or directory locally and stage server deletions for the next dbo push')
|
|
@@ -41,7 +43,7 @@ export const rmCommand = new Command('rm')
|
|
|
41
43
|
* Resolve a file path to its metadata.json path.
|
|
42
44
|
*/
|
|
43
45
|
function resolveMetaPath(filePath) {
|
|
44
|
-
if (filePath.endsWith('.metadata.json')) {
|
|
46
|
+
if (isMetadataFile(basename(filePath)) || filePath.endsWith('.metadata.json')) {
|
|
45
47
|
return filePath;
|
|
46
48
|
}
|
|
47
49
|
const dir = dirname(filePath);
|
|
@@ -100,7 +102,7 @@ async function rmFileRecord(metaPath, options, { skipPrompt = false } = {}) {
|
|
|
100
102
|
localFiles.push(join(metaDir, String(meta._mediaFile).substring(1)));
|
|
101
103
|
}
|
|
102
104
|
|
|
103
|
-
const displayName = basename(metaPath, '.metadata.json');
|
|
105
|
+
const displayName = parseMetaFilename(basename(metaPath))?.naturalBase ?? basename(metaPath, '.metadata.json');
|
|
104
106
|
|
|
105
107
|
// Prompt if needed
|
|
106
108
|
if (!skipPrompt && !options.force) {
|
|
@@ -120,6 +122,7 @@ async function rmFileRecord(metaPath, options, { skipPrompt = false } = {}) {
|
|
|
120
122
|
// Stage deletion (include metaPath for Trash workflow in push.js)
|
|
121
123
|
const expression = `RowID:del${rowId};entity:${entity}=true`;
|
|
122
124
|
await addDeleteEntry({ UID: uid, RowID: rowId, entity, name: displayName, expression, metaPath });
|
|
125
|
+
await removeDeployEntry(uid);
|
|
123
126
|
log.success(` Staged: ${displayName} → ${expression}`);
|
|
124
127
|
|
|
125
128
|
// Remove from app.json
|
|
@@ -156,14 +159,26 @@ async function rmFileRecord(metaPath, options, { skipPrompt = false } = {}) {
|
|
|
156
159
|
* Remove a single file (entry point for non-directory rm).
|
|
157
160
|
*/
|
|
158
161
|
async function rmFile(filePath, options) {
|
|
159
|
-
|
|
162
|
+
let metaPath = resolveMetaPath(filePath);
|
|
160
163
|
|
|
161
164
|
let meta;
|
|
162
165
|
try {
|
|
163
166
|
meta = JSON.parse(await readFile(metaPath, 'utf8'));
|
|
164
167
|
} catch {
|
|
165
|
-
|
|
166
|
-
|
|
168
|
+
// Direct metadata path not found — search by @reference
|
|
169
|
+
const found = await findMetadataForCompanion(filePath);
|
|
170
|
+
if (found) {
|
|
171
|
+
metaPath = found;
|
|
172
|
+
try {
|
|
173
|
+
meta = JSON.parse(await readFile(metaPath, 'utf8'));
|
|
174
|
+
} catch {
|
|
175
|
+
log.error(`Could not read metadata at "${metaPath}".`);
|
|
176
|
+
process.exit(1);
|
|
177
|
+
}
|
|
178
|
+
} else {
|
|
179
|
+
log.error(`No metadata found for "${basename(filePath)}". Cannot determine record to delete.`);
|
|
180
|
+
process.exit(1);
|
|
181
|
+
}
|
|
167
182
|
}
|
|
168
183
|
|
|
169
184
|
const entity = meta._entity;
|
|
@@ -192,7 +207,7 @@ async function rmFile(filePath, options) {
|
|
|
192
207
|
localFiles.push(join(metaDir, String(meta._mediaFile).substring(1)));
|
|
193
208
|
}
|
|
194
209
|
|
|
195
|
-
const displayName = basename(metaPath, '.metadata.json');
|
|
210
|
+
const displayName = parseMetaFilename(basename(metaPath))?.naturalBase ?? basename(metaPath, '.metadata.json');
|
|
196
211
|
log.info(`Removing "${displayName}" (${entity}:${uid || rowId})`);
|
|
197
212
|
for (const f of localFiles) {
|
|
198
213
|
log.dim(` ${f}`);
|