@dboio/cli 0.11.3 → 0.13.2
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 +126 -3
- package/bin/dbo.js +4 -0
- package/package.json +1 -1
- package/plugins/claude/dbo/.claude-plugin/plugin.json +1 -1
- package/plugins/claude/dbo/commands/dbo.md +65 -244
- 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 +2222 -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 +62 -246
- package/src/commands/add.js +366 -62
- package/src/commands/build.js +102 -0
- package/src/commands/clone.js +602 -139
- package/src/commands/diff.js +4 -0
- package/src/commands/init.js +16 -2
- package/src/commands/input.js +3 -1
- package/src/commands/mv.js +12 -4
- package/src/commands/push.js +265 -70
- package/src/commands/rm.js +16 -3
- package/src/commands/run.js +81 -0
- package/src/lib/client.js +4 -7
- package/src/lib/config.js +39 -0
- package/src/lib/delta.js +7 -1
- package/src/lib/diff.js +24 -2
- package/src/lib/filenames.js +120 -41
- package/src/lib/ignore.js +6 -0
- package/src/lib/input-parser.js +13 -4
- package/src/lib/scripts.js +232 -0
- package/src/lib/toe-stepping.js +17 -2
- package/src/migrations/006-remove-uid-companion-filenames.js +181 -0
package/src/commands/diff.js
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
} from '../lib/diff.js';
|
|
15
15
|
import { fetchServerRecordsBatch } from '../lib/toe-stepping.js';
|
|
16
16
|
import { findBaselineEntry } from '../lib/delta.js';
|
|
17
|
+
import { findMetadataForCompanion } from '../lib/filenames.js';
|
|
17
18
|
import { runPendingMigrations } from '../lib/migrations.js';
|
|
18
19
|
|
|
19
20
|
export const diffCommand = new Command('diff')
|
|
@@ -239,6 +240,9 @@ async function resolveTargetToMetaFiles(targetPath) {
|
|
|
239
240
|
await stat(mediaMetaPath);
|
|
240
241
|
return [mediaMetaPath];
|
|
241
242
|
} catch {
|
|
243
|
+
// Fallback: scan sibling metadata files for @reference match
|
|
244
|
+
const found = await findMetadataForCompanion(targetPath);
|
|
245
|
+
if (found) return [found];
|
|
242
246
|
log.warn(`No metadata found for "${targetPath}"`);
|
|
243
247
|
return [];
|
|
244
248
|
}
|
package/src/commands/init.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
|
-
import { mkdir } from 'fs/promises';
|
|
2
|
+
import { mkdir, writeFile, access } from 'fs/promises';
|
|
3
3
|
import { join } from 'path';
|
|
4
4
|
import { isInitialized, hasLegacyConfig, readLegacyConfig, initConfig, saveCredentials, ensureGitignore, saveTransactionKeyPreset, saveTicketSuggestionOutput } from '../lib/config.js';
|
|
5
5
|
import { installOrUpdateClaudeCommands } from './install.js';
|
|
@@ -102,7 +102,7 @@ export const initCommand = new Command('init')
|
|
|
102
102
|
}
|
|
103
103
|
|
|
104
104
|
// Ensure sensitive files are gitignored
|
|
105
|
-
await ensureGitignore(['.dbo/credentials.json', '.dbo/cookies.txt', '.dbo/config.local.json', '.dbo/ticketing.local.json', '.dbo/errors.log', 'trash/', 'Icon\\r']);
|
|
105
|
+
await ensureGitignore(['.dbo/credentials.json', '.dbo/cookies.txt', '.dbo/config.local.json', '.dbo/ticketing.local.json', '.dbo/scripts.local.json', '.dbo/errors.log', 'trash/', 'Icon\\r']);
|
|
106
106
|
|
|
107
107
|
const createdIgnore = await createDboignore();
|
|
108
108
|
if (createdIgnore) log.dim(' Created .dboignore');
|
|
@@ -114,6 +114,20 @@ export const initCommand = new Command('init')
|
|
|
114
114
|
}
|
|
115
115
|
log.dim(' Created .claude/ directory structure');
|
|
116
116
|
|
|
117
|
+
// Create empty scripts.json and scripts.local.json if they don't exist
|
|
118
|
+
const emptyScripts = JSON.stringify({ scripts: {}, targets: {}, entities: {} }, null, 2) + '\n';
|
|
119
|
+
const dboDir = join(process.cwd(), '.dbo');
|
|
120
|
+
const scriptsPath = join(dboDir, 'scripts.json');
|
|
121
|
+
const scriptsLocalPath = join(dboDir, 'scripts.local.json');
|
|
122
|
+
try { await access(scriptsPath); } catch {
|
|
123
|
+
await writeFile(scriptsPath, emptyScripts);
|
|
124
|
+
log.dim(' Created .dbo/scripts.json');
|
|
125
|
+
}
|
|
126
|
+
try { await access(scriptsLocalPath); } catch {
|
|
127
|
+
await writeFile(scriptsLocalPath, emptyScripts);
|
|
128
|
+
log.dim(' Created .dbo/scripts.local.json');
|
|
129
|
+
}
|
|
130
|
+
|
|
117
131
|
log.success(`Initialized .dbo/ for ${domain}`);
|
|
118
132
|
|
|
119
133
|
// Authenticate early so the session is ready for subsequent operations
|
package/src/commands/input.js
CHANGED
|
@@ -60,8 +60,10 @@ export const inputCommand = new Command('input')
|
|
|
60
60
|
if (modifyKeyResult.modifyKey) extraParams['_modify_key'] = modifyKeyResult.modifyKey;
|
|
61
61
|
|
|
62
62
|
// Check if data expressions include AppID; if not and config has one, prompt
|
|
63
|
+
// Skip AppID prompt for delete-only submissions — deletes don't need it
|
|
63
64
|
const allDataText = options.data.join(' ');
|
|
64
|
-
const
|
|
65
|
+
const isDeleteOnly = options.data.every(d => /RowID:del\d+/.test(d));
|
|
66
|
+
const hasAppId = isDeleteOnly || /\.AppID[=@]/.test(allDataText) || /AppID=/.test(allDataText);
|
|
65
67
|
if (!hasAppId) {
|
|
66
68
|
const appConfig = await loadAppConfig();
|
|
67
69
|
if (appConfig.AppID) {
|
package/src/commands/mv.js
CHANGED
|
@@ -13,6 +13,7 @@ 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
17
|
import { runPendingMigrations } from '../lib/migrations.js';
|
|
17
18
|
|
|
18
19
|
export const mvCommand = new Command('mv')
|
|
@@ -507,14 +508,21 @@ async function checkNameConflict(fileName, targetDir, options) {
|
|
|
507
508
|
*/
|
|
508
509
|
async function mvFile(sourceFile, targetBinId, structure, options) {
|
|
509
510
|
// Resolve metadata
|
|
510
|
-
|
|
511
|
+
let metaPath = resolveMetaPath(sourceFile);
|
|
511
512
|
let meta;
|
|
512
513
|
try {
|
|
513
514
|
meta = JSON.parse(await readFile(metaPath, 'utf8'));
|
|
514
515
|
} catch {
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
516
|
+
const found = await findMetadataForCompanion(sourceFile);
|
|
517
|
+
if (found) {
|
|
518
|
+
metaPath = found;
|
|
519
|
+
try { meta = JSON.parse(await readFile(metaPath, 'utf8')); } catch {}
|
|
520
|
+
}
|
|
521
|
+
if (!meta) {
|
|
522
|
+
log.error(`No metadata found for "${basename(sourceFile)}"`);
|
|
523
|
+
log.dim(' Use "dbo pull" to create metadata files.');
|
|
524
|
+
process.exit(1);
|
|
525
|
+
}
|
|
518
526
|
}
|
|
519
527
|
|
|
520
528
|
const entity = meta._entity;
|
package/src/commands/push.js
CHANGED
|
@@ -6,12 +6,13 @@ 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, 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';
|
|
@@ -19,6 +20,7 @@ import { BINS_DIR, ENTITY_DIR_NAMES, loadStructureFile, findBinByPath } from '..
|
|
|
19
20
|
import { ensureTrashIcon } from '../lib/folder-icon.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';
|
|
22
24
|
|
|
23
25
|
/**
|
|
24
26
|
* Resolve an @reference file path to an absolute filesystem path.
|
|
@@ -42,9 +44,16 @@ function isToeStepping(options) {
|
|
|
42
44
|
|
|
43
45
|
import { ENTITY_DEPENDENCIES } from '../lib/dependencies.js';
|
|
44
46
|
|
|
47
|
+
async function loadAndMergeScripts() {
|
|
48
|
+
const base = await loadScripts();
|
|
49
|
+
const local = await loadScriptsLocal();
|
|
50
|
+
if (!base && !local) return null;
|
|
51
|
+
return mergeScriptsConfig(base, local);
|
|
52
|
+
}
|
|
53
|
+
|
|
45
54
|
export const pushCommand = new Command('push')
|
|
46
55
|
.description('Push local files back to DBO.io using metadata from pull')
|
|
47
|
-
.argument('
|
|
56
|
+
.argument('[path]', 'File or directory to push (default: current directory)')
|
|
48
57
|
.option('-C, --confirm <value>', 'Commit: true (default) or false', 'true')
|
|
49
58
|
.option('--ticket <id>', 'Override ticket ID')
|
|
50
59
|
.option('--modify-key <key>', 'Provide ModifyKey directly (skips interactive prompt)')
|
|
@@ -58,7 +67,9 @@ export const pushCommand = new Command('push')
|
|
|
58
67
|
.option('-v, --verbose', 'Show HTTP request details')
|
|
59
68
|
.option('--domain <host>', 'Override domain')
|
|
60
69
|
.option('--no-migrate', 'Skip pending migrations for this invocation')
|
|
61
|
-
.
|
|
70
|
+
.option('--no-scripts', 'Bypass all script hooks; run default push pipeline unconditionally')
|
|
71
|
+
.option('--no-build', 'Skip the build phase (prebuild/build/postbuild); run push phase only')
|
|
72
|
+
.action(async (targetPath = ".", options) => {
|
|
62
73
|
try {
|
|
63
74
|
await runPendingMigrations(options);
|
|
64
75
|
const client = new DboClient({ domain: options.domain, verbose: options.verbose });
|
|
@@ -169,18 +180,29 @@ async function processPendingDeletes(client, options, modifyKey = null, transact
|
|
|
169
180
|
const body = await buildInputBody([entry.expression], extraParams);
|
|
170
181
|
|
|
171
182
|
try {
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
//
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
183
|
+
let result = await client.postUrlEncoded('/api/input/submit', body);
|
|
184
|
+
|
|
185
|
+
// Retry with prompted params if needed (ticket, repo mismatch, user)
|
|
186
|
+
if (!result.successful) {
|
|
187
|
+
const retryResult = await checkSubmitErrors(result, { rowUid: entry.UID });
|
|
188
|
+
if (retryResult) {
|
|
189
|
+
if (retryResult.skipRecord) { remaining.push(entry); continue; }
|
|
190
|
+
if (retryResult.skipAll) break;
|
|
191
|
+
if (retryResult.ticketExpressions?.length > 0) {
|
|
192
|
+
// Re-build body with ticket expressions added to the delete expression
|
|
193
|
+
const allExprs = [entry.expression, ...retryResult.ticketExpressions];
|
|
194
|
+
const retryParams = { ...extraParams, ...(retryResult.retryParams || retryResult) };
|
|
195
|
+
const retryBody = await buildInputBody(allExprs, retryParams);
|
|
196
|
+
result = await client.postUrlEncoded('/api/input/submit', retryBody);
|
|
197
|
+
} else {
|
|
198
|
+
const retryParams = { ...extraParams, ...(retryResult.retryParams || retryResult) };
|
|
199
|
+
const retryBody = await buildInputBody([entry.expression], retryParams);
|
|
200
|
+
result = await client.postUrlEncoded('/api/input/submit', retryBody);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (result.successful) {
|
|
184
206
|
log.success(` Deleted "${entry.name}" from server`);
|
|
185
207
|
deletedUids.push(entry.UID);
|
|
186
208
|
} else {
|
|
@@ -297,8 +319,35 @@ async function pushSingleFile(filePath, client, options, modifyKey = null, trans
|
|
|
297
319
|
try {
|
|
298
320
|
meta = JSON.parse(await readFile(metaPath, 'utf8'));
|
|
299
321
|
} catch {
|
|
300
|
-
|
|
301
|
-
|
|
322
|
+
// Direct metadata path not found — search by @reference
|
|
323
|
+
const found = await findMetadataForCompanion(filePath);
|
|
324
|
+
if (found) {
|
|
325
|
+
metaPath = found;
|
|
326
|
+
try { meta = JSON.parse(await readFile(metaPath, 'utf8')); } catch {}
|
|
327
|
+
}
|
|
328
|
+
if (!meta) {
|
|
329
|
+
// Try auto-detecting as a bin content/media file and add it first
|
|
330
|
+
const binMeta = await detectBinFile(filePath);
|
|
331
|
+
if (binMeta) {
|
|
332
|
+
log.info(`No metadata found — auto-adding "${basename(filePath)}" first`);
|
|
333
|
+
try {
|
|
334
|
+
await submitAdd(binMeta.meta, binMeta.metaPath, filePath, client, options);
|
|
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
|
+
}
|
|
350
|
+
}
|
|
302
351
|
}
|
|
303
352
|
|
|
304
353
|
// Toe-stepping check for single-file push
|
|
@@ -311,6 +360,41 @@ async function pushSingleFile(filePath, client, options, modifyKey = null, trans
|
|
|
311
360
|
}
|
|
312
361
|
}
|
|
313
362
|
|
|
363
|
+
// ── Script hooks ────────────────────────────────────────────────────
|
|
364
|
+
if (options.scripts !== false) {
|
|
365
|
+
const scriptsConfig = await loadAndMergeScripts();
|
|
366
|
+
if (scriptsConfig) {
|
|
367
|
+
const relPath = relative(process.cwd(), filePath).replace(/\\/g, '/');
|
|
368
|
+
const entityType = meta._entity || '';
|
|
369
|
+
const hooks = resolveHooks(relPath, entityType, scriptsConfig);
|
|
370
|
+
const cfg = await loadConfig();
|
|
371
|
+
const app = await loadAppConfig();
|
|
372
|
+
const env = buildHookEnv(relPath, entityType, { ...app, domain: cfg.domain });
|
|
373
|
+
|
|
374
|
+
// Build phase
|
|
375
|
+
if (options.build !== false && (hooks.prebuild !== undefined || hooks.build !== undefined || hooks.postbuild !== undefined)) {
|
|
376
|
+
log.dim(` Running build hooks for ${basename(filePath)}...`);
|
|
377
|
+
const buildOk = await runBuildLifecycle(hooks, env, process.cwd());
|
|
378
|
+
if (!buildOk) {
|
|
379
|
+
log.error(`Build hook failed for "${basename(filePath)}" — aborting push`);
|
|
380
|
+
process.exit(1);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Push phase
|
|
385
|
+
const pushResult = await runPushLifecycle(hooks, env, process.cwd());
|
|
386
|
+
if (pushResult.failed) {
|
|
387
|
+
log.error(`Push hook failed for "${basename(filePath)}" — aborting`);
|
|
388
|
+
process.exit(1);
|
|
389
|
+
}
|
|
390
|
+
if (!pushResult.runDefault) {
|
|
391
|
+
log.dim(` Skipped default push for "${basename(filePath)}" (custom push hook handled it)`);
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
// ── End script hooks ────────────────────────────────────────────────
|
|
397
|
+
|
|
314
398
|
await pushFromMetadata(meta, metaPath, client, options, null, modifyKey, transactionKey);
|
|
315
399
|
}
|
|
316
400
|
/**
|
|
@@ -328,13 +412,16 @@ async function ensureManifestMetadata() {
|
|
|
328
412
|
|
|
329
413
|
// Scan the entire project for any metadata file that already references manifest.json.
|
|
330
414
|
// This prevents creating duplicates when the metadata lives in an unexpected location.
|
|
415
|
+
// Check both @/manifest.json (root-relative) and @manifest.json (local) references,
|
|
416
|
+
// as well as Path: manifest.json which indicates a server record for this file.
|
|
331
417
|
const ig = await loadIgnore();
|
|
332
418
|
const allMeta = await findMetadataFiles(process.cwd(), ig);
|
|
333
419
|
for (const metaPath of allMeta) {
|
|
334
420
|
try {
|
|
335
421
|
const raw = await readFile(metaPath, 'utf8');
|
|
336
422
|
const parsed = JSON.parse(raw);
|
|
337
|
-
if (parsed.Content === '@/manifest.json') return;
|
|
423
|
+
if (parsed.Content === '@/manifest.json' || parsed.Content === '@manifest.json') return;
|
|
424
|
+
if (parsed.Path === 'manifest.json' || parsed.Path === '/manifest.json') return;
|
|
338
425
|
} catch { /* skip unreadable */ }
|
|
339
426
|
}
|
|
340
427
|
|
|
@@ -373,10 +460,116 @@ async function pushDirectory(dirPath, client, options, modifyKey = null, transac
|
|
|
373
460
|
// Auto-create manifest.metadata.json if manifest.json exists at root without companion metadata
|
|
374
461
|
await ensureManifestMetadata();
|
|
375
462
|
|
|
463
|
+
// ── Auto-add: detect un-added files and create+submit them before push ──
|
|
376
464
|
const ig = await loadIgnore();
|
|
465
|
+
const justAddedUIDs = new Set(); // Track UIDs added in this invocation — skip from push loop
|
|
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
|
+
}
|
|
513
|
+
|
|
377
514
|
const metaFiles = await findMetadataFiles(dirPath, ig);
|
|
378
515
|
|
|
379
|
-
// Load
|
|
516
|
+
// ── Load scripts config early (before delta detection) ──────────────
|
|
517
|
+
// Build hooks must run BEFORE delta detection so compiled output files
|
|
518
|
+
// (SASS → CSS, Rollup → JS, etc.) are on disk when changes are detected.
|
|
519
|
+
let scriptsConfig = null;
|
|
520
|
+
let appConfigForHooks = null;
|
|
521
|
+
if (options.scripts !== false) {
|
|
522
|
+
scriptsConfig = await loadAndMergeScripts();
|
|
523
|
+
if (scriptsConfig) {
|
|
524
|
+
const cfg = await loadConfig();
|
|
525
|
+
const app = await loadAppConfig();
|
|
526
|
+
appConfigForHooks = { ...app, domain: cfg.domain };
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// ── Run build phase upfront (before delta detection) ────────────────
|
|
531
|
+
if (scriptsConfig && options.build !== false) {
|
|
532
|
+
const globalPrebuild = scriptsConfig.scripts?.prebuild;
|
|
533
|
+
const globalBuild = scriptsConfig.scripts?.build;
|
|
534
|
+
const globalPostbuild = scriptsConfig.scripts?.postbuild;
|
|
535
|
+
const globalHasAnyBuild = globalPrebuild !== undefined || globalBuild !== undefined || globalPostbuild !== undefined;
|
|
536
|
+
|
|
537
|
+
// 1. Run global build hooks once
|
|
538
|
+
if (globalHasAnyBuild) {
|
|
539
|
+
const globalHooks = { prebuild: globalPrebuild, build: globalBuild, postbuild: globalPostbuild };
|
|
540
|
+
const env = buildHookEnv('', '', appConfigForHooks);
|
|
541
|
+
log.dim(' Running global build hooks...');
|
|
542
|
+
const ok = await runBuildLifecycle(globalHooks, env, process.cwd());
|
|
543
|
+
if (!ok) {
|
|
544
|
+
log.error('Global build hook failed — aborting push');
|
|
545
|
+
process.exit(1);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// 2. Run per-target/entity build hooks for each metadata file
|
|
550
|
+
// (only when the resolved hook differs from the global — avoids re-running global)
|
|
551
|
+
for (const metaPath of metaFiles) {
|
|
552
|
+
let meta;
|
|
553
|
+
try { meta = JSON.parse(await readFile(metaPath, 'utf8')); } catch { continue; }
|
|
554
|
+
const relPath = relative(process.cwd(), metaPath.replace(/\.metadata\.json$/, '')).replace(/\\/g, '/');
|
|
555
|
+
const entityType = meta._entity || '';
|
|
556
|
+
const hooks = resolveHooks(relPath, entityType, scriptsConfig);
|
|
557
|
+
const hasNonGlobalBuild = (hooks.prebuild !== undefined && hooks.prebuild !== globalPrebuild)
|
|
558
|
+
|| (hooks.build !== undefined && hooks.build !== globalBuild)
|
|
559
|
+
|| (hooks.postbuild !== undefined && hooks.postbuild !== globalPostbuild);
|
|
560
|
+
if (hasNonGlobalBuild) {
|
|
561
|
+
const env = buildHookEnv(relPath, entityType, appConfigForHooks);
|
|
562
|
+
log.dim(` Build hooks: ${relPath}`);
|
|
563
|
+
const ok = await runBuildLifecycle(hooks, env, process.cwd());
|
|
564
|
+
if (!ok) {
|
|
565
|
+
log.error(`Build hook failed for "${basename(metaPath)}" — aborting push`);
|
|
566
|
+
process.exit(1);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// Load baseline for delta detection (after build hooks so compiled files are on disk)
|
|
380
573
|
const baseline = await loadAppJsonBaseline();
|
|
381
574
|
|
|
382
575
|
if (!baseline) {
|
|
@@ -404,6 +597,12 @@ async function pushDirectory(dirPath, client, options, modifyKey = null, transac
|
|
|
404
597
|
continue;
|
|
405
598
|
}
|
|
406
599
|
|
|
600
|
+
// Skip records that were just auto-added in this invocation — they're already on the server
|
|
601
|
+
if (meta.UID && justAddedUIDs.has(meta.UID)) {
|
|
602
|
+
log.dim(` Skipped (just added): ${basename(metaPath)}`);
|
|
603
|
+
continue;
|
|
604
|
+
}
|
|
605
|
+
|
|
407
606
|
// Compound output files: handle root + all inline children together
|
|
408
607
|
// These have _entity='output' and inline children under .children
|
|
409
608
|
if (meta._entity === 'output' && meta.children) {
|
|
@@ -556,6 +755,19 @@ async function pushDirectory(dirPath, client, options, modifyKey = null, transac
|
|
|
556
755
|
}
|
|
557
756
|
for (const item of toAdd) {
|
|
558
757
|
try {
|
|
758
|
+
// Run push hooks for this item (build hooks already ran upfront)
|
|
759
|
+
if (scriptsConfig) {
|
|
760
|
+
const relPath = relative(process.cwd(), item.metaPath.replace(/\.metadata\.json$/, '')).replace(/\\/g, '/');
|
|
761
|
+
const entityType = item.meta._entity || '';
|
|
762
|
+
const hooks = resolveHooks(relPath, entityType, scriptsConfig);
|
|
763
|
+
if (hooks.prepush !== undefined || hooks.push !== undefined || hooks.postpush !== undefined) {
|
|
764
|
+
const env = buildHookEnv(relPath, entityType, appConfigForHooks);
|
|
765
|
+
const pushResult = await runPushLifecycle(hooks, env, process.cwd());
|
|
766
|
+
if (pushResult.failed) { log.error(`Push hook failed for "${basename(item.metaPath)}" — aborting`); failed++; break; }
|
|
767
|
+
if (!pushResult.runDefault) { succeeded++; successfulPushes.push(item); continue; }
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
|
|
559
771
|
const success = await addFromMetadata(item.meta, item.metaPath, client, options, modifyKey);
|
|
560
772
|
if (success) {
|
|
561
773
|
succeeded++;
|
|
@@ -576,6 +788,19 @@ async function pushDirectory(dirPath, client, options, modifyKey = null, transac
|
|
|
576
788
|
// Then process edits
|
|
577
789
|
for (const item of toEdit) {
|
|
578
790
|
try {
|
|
791
|
+
// Run push hooks for this item (build hooks already ran upfront)
|
|
792
|
+
if (scriptsConfig) {
|
|
793
|
+
const relPath = relative(process.cwd(), item.metaPath.replace(/\.metadata\.json$/, '')).replace(/\\/g, '/');
|
|
794
|
+
const entityType = item.meta._entity || '';
|
|
795
|
+
const hooks = resolveHooks(relPath, entityType, scriptsConfig);
|
|
796
|
+
if (hooks.prepush !== undefined || hooks.push !== undefined || hooks.postpush !== undefined) {
|
|
797
|
+
const env = buildHookEnv(relPath, entityType, appConfigForHooks);
|
|
798
|
+
const pushResult = await runPushLifecycle(hooks, env, process.cwd());
|
|
799
|
+
if (pushResult.failed) { log.error(`Push hook failed for "${basename(item.metaPath)}" — aborting`); failed++; break; }
|
|
800
|
+
if (!pushResult.runDefault) { succeeded++; successfulPushes.push(item); continue; }
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
|
|
579
804
|
const success = await pushFromMetadata(item.meta, item.metaPath, client, options, item.changedColumns, modifyKey, transactionKey);
|
|
580
805
|
if (success) {
|
|
581
806
|
succeeded++;
|
|
@@ -866,66 +1091,31 @@ async function addFromMetadata(meta, metaPath, client, options, modifyKey = null
|
|
|
866
1091
|
return false;
|
|
867
1092
|
}
|
|
868
1093
|
|
|
869
|
-
// Extract UID from response and rename
|
|
1094
|
+
// Extract UID from response and rename metadata to ~uid convention
|
|
870
1095
|
const addResults = result.payload?.Results?.Add || result.data?.Payload?.Results?.Add || [];
|
|
871
1096
|
if (addResults.length > 0) {
|
|
872
1097
|
const returnedUID = addResults[0].UID;
|
|
873
1098
|
const returnedLastUpdated = addResults[0]._LastUpdated;
|
|
874
1099
|
|
|
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');
|
|
884
|
-
|
|
885
|
-
// Guard: don't append UID if it's already in the filename
|
|
886
|
-
if (hasUidInFilename(currentMetaBase, returnedUID)) {
|
|
887
|
-
await writeFile(metaPath, JSON.stringify(meta, null, 2) + '\n');
|
|
888
|
-
log.success(`UID ${returnedUID} already in filename`);
|
|
889
|
-
return true;
|
|
890
|
-
}
|
|
1100
|
+
// Store numeric ID for delete operations (RowID:del<id>)
|
|
1101
|
+
const entityIdKey = entity.charAt(0).toUpperCase() + entity.slice(1) + 'ID';
|
|
1102
|
+
const returnedId = addResults[0][entityIdKey] || addResults[0]._id || addResults[0].ID;
|
|
1103
|
+
if (returnedId) meta._id = returnedId;
|
|
891
1104
|
|
|
892
|
-
|
|
893
|
-
|
|
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
|
-
}
|
|
1105
|
+
// Write _LastUpdated back to meta so baseline gets it
|
|
1106
|
+
if (returnedLastUpdated) meta._LastUpdated = returnedLastUpdated;
|
|
914
1107
|
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
try { await fsRename(metaPath, newMetaPath); } catch { /* ignore if same */ }
|
|
918
|
-
}
|
|
919
|
-
await writeFile(newMetaPath, JSON.stringify(meta, null, 2) + '\n');
|
|
1108
|
+
if (returnedUID) {
|
|
1109
|
+
meta.UID = returnedUID;
|
|
920
1110
|
|
|
921
|
-
// Set timestamps from server
|
|
922
1111
|
const config = await loadConfig();
|
|
923
1112
|
const serverTz = config.ServerTimezone;
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
1113
|
+
|
|
1114
|
+
// Rename metadata file to ~UID convention; companions keep natural names
|
|
1115
|
+
const renameResult = await renameToUidConvention(meta, metaPath, returnedUID, returnedLastUpdated, serverTz);
|
|
1116
|
+
|
|
1117
|
+
// Propagate updated meta back (renameToUidConvention creates a new object)
|
|
1118
|
+
Object.assign(meta, renameResult.updatedMeta);
|
|
929
1119
|
|
|
930
1120
|
log.success(`Added: ${basename(metaPath)} → UID ${returnedUID}`);
|
|
931
1121
|
}
|
|
@@ -1469,6 +1659,11 @@ async function _submitOutputEntity(entity, physicalEntity, changedColumns, metaD
|
|
|
1469
1659
|
const strValue = (val === null || val === undefined) ? '' : String(val);
|
|
1470
1660
|
if (isReference(strValue)) {
|
|
1471
1661
|
const refPath = resolveReferencePath(strValue, metaDir);
|
|
1662
|
+
// Skip missing companion files (e.g. empty CustomSQL not extracted by clone)
|
|
1663
|
+
try { await access(refPath); } catch {
|
|
1664
|
+
log.dim(` Skipping ${col} — companion file not found: ${basename(refPath)}`);
|
|
1665
|
+
continue;
|
|
1666
|
+
}
|
|
1472
1667
|
dataExprs.push(`${rowKeyPrefix}:${rowKeyValue};column:${physicalEntity}.${col}@${refPath}`);
|
|
1473
1668
|
} else {
|
|
1474
1669
|
dataExprs.push(`${rowKeyPrefix}:${rowKeyValue};column:${physicalEntity}.${col}=${strValue}`);
|
package/src/commands/rm.js
CHANGED
|
@@ -5,6 +5,7 @@ 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
9
|
import { loadStructureFile, findBinByPath, findChildBins, BINS_DIR } from '../lib/structure.js';
|
|
9
10
|
import { runPendingMigrations } from '../lib/migrations.js';
|
|
10
11
|
|
|
@@ -156,14 +157,26 @@ async function rmFileRecord(metaPath, options, { skipPrompt = false } = {}) {
|
|
|
156
157
|
* Remove a single file (entry point for non-directory rm).
|
|
157
158
|
*/
|
|
158
159
|
async function rmFile(filePath, options) {
|
|
159
|
-
|
|
160
|
+
let metaPath = resolveMetaPath(filePath);
|
|
160
161
|
|
|
161
162
|
let meta;
|
|
162
163
|
try {
|
|
163
164
|
meta = JSON.parse(await readFile(metaPath, 'utf8'));
|
|
164
165
|
} catch {
|
|
165
|
-
|
|
166
|
-
|
|
166
|
+
// Direct metadata path not found — search by @reference
|
|
167
|
+
const found = await findMetadataForCompanion(filePath);
|
|
168
|
+
if (found) {
|
|
169
|
+
metaPath = found;
|
|
170
|
+
try {
|
|
171
|
+
meta = JSON.parse(await readFile(metaPath, 'utf8'));
|
|
172
|
+
} catch {
|
|
173
|
+
log.error(`Could not read metadata at "${metaPath}".`);
|
|
174
|
+
process.exit(1);
|
|
175
|
+
}
|
|
176
|
+
} else {
|
|
177
|
+
log.error(`No metadata found for "${basename(filePath)}". Cannot determine record to delete.`);
|
|
178
|
+
process.exit(1);
|
|
179
|
+
}
|
|
167
180
|
}
|
|
168
181
|
|
|
169
182
|
const entity = meta._entity;
|