@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
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// src/commands/run.js
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import { loadScripts, loadScriptsLocal, loadConfig, loadAppConfig } from '../lib/config.js';
|
|
4
|
+
import { mergeScriptsConfig, buildHookEnv, runHook } from '../lib/scripts.js';
|
|
5
|
+
import { log } from '../lib/logger.js';
|
|
6
|
+
|
|
7
|
+
export const runCommand = new Command('run')
|
|
8
|
+
.description('Run a named script from .dbo/scripts.json (like npm run)')
|
|
9
|
+
.argument('[script-name]', 'Name of the script to run (omit to list all scripts)')
|
|
10
|
+
.action(async (scriptName, options) => {
|
|
11
|
+
try {
|
|
12
|
+
const base = await loadScripts();
|
|
13
|
+
const local = await loadScriptsLocal();
|
|
14
|
+
|
|
15
|
+
if (!base && !local) {
|
|
16
|
+
log.error('No .dbo/scripts.json found');
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const config = mergeScriptsConfig(base, local);
|
|
21
|
+
const scripts = config.scripts || {};
|
|
22
|
+
|
|
23
|
+
if (!scriptName) {
|
|
24
|
+
// List all script names
|
|
25
|
+
const allNames = Object.keys(scripts);
|
|
26
|
+
if (allNames.length === 0) {
|
|
27
|
+
log.info('No scripts defined in .dbo/scripts.json');
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
log.info('Available scripts:');
|
|
31
|
+
for (const name of allNames) {
|
|
32
|
+
const value = scripts[name];
|
|
33
|
+
const preview = Array.isArray(value) ? value[0] : (value === false ? '(skip)' : String(value));
|
|
34
|
+
log.plain(` ${name}: ${preview}`);
|
|
35
|
+
}
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (!(scriptName in scripts)) {
|
|
40
|
+
log.error(`Script "${scriptName}" not found in .dbo/scripts.json`);
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const cfg = await loadConfig();
|
|
45
|
+
const app = await loadAppConfig();
|
|
46
|
+
const appConfig = { ...app, domain: cfg.domain };
|
|
47
|
+
const env = buildHookEnv('', scriptName, appConfig);
|
|
48
|
+
|
|
49
|
+
// Pre/post auto-resolution
|
|
50
|
+
const preKey = `pre${scriptName}`;
|
|
51
|
+
const postKey = `post${scriptName}`;
|
|
52
|
+
|
|
53
|
+
if (preKey in scripts) {
|
|
54
|
+
log.dim(` Running ${preKey}...`);
|
|
55
|
+
const result = await runHook(scripts[preKey], env, { cwd: process.cwd() });
|
|
56
|
+
if (!result.skipped && result.exitCode !== 0 && result.exitCode !== null) {
|
|
57
|
+
log.error(`Script "${preKey}" failed with exit code ${result.exitCode}`);
|
|
58
|
+
process.exit(result.exitCode);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
log.dim(` Running ${scriptName}...`);
|
|
63
|
+
const result = await runHook(scripts[scriptName], env, { cwd: process.cwd() });
|
|
64
|
+
if (!result.skipped && result.exitCode !== 0 && result.exitCode !== null) {
|
|
65
|
+
log.error(`Script "${scriptName}" failed with exit code ${result.exitCode}`);
|
|
66
|
+
process.exit(result.exitCode);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (postKey in scripts) {
|
|
70
|
+
log.dim(` Running ${postKey}...`);
|
|
71
|
+
const postResult = await runHook(scripts[postKey], env, { cwd: process.cwd() });
|
|
72
|
+
if (!postResult.skipped && postResult.exitCode !== 0 && postResult.exitCode !== null) {
|
|
73
|
+
log.error(`Script "${postKey}" failed with exit code ${postResult.exitCode}`);
|
|
74
|
+
process.exit(postResult.exitCode);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
} catch (err) {
|
|
78
|
+
log.error(err.message);
|
|
79
|
+
process.exit(1);
|
|
80
|
+
}
|
|
81
|
+
});
|
package/src/lib/client.js
CHANGED
|
@@ -124,15 +124,12 @@ export class DboClient {
|
|
|
124
124
|
}
|
|
125
125
|
|
|
126
126
|
/**
|
|
127
|
-
*
|
|
128
|
-
*
|
|
127
|
+
* No-op placeholder. Server-side cache voiding (/?voidcache=true) has been
|
|
128
|
+
* disabled as it was causing server issues. Callers remain unchanged so the
|
|
129
|
+
* hook can be re-enabled later if needed.
|
|
129
130
|
*/
|
|
130
131
|
async voidCache() {
|
|
131
|
-
|
|
132
|
-
const baseUrl = await this.getBaseUrl();
|
|
133
|
-
if (this.verbose) log.verbose(`VoidCache: ${baseUrl}/?voidcache=true`);
|
|
134
|
-
await this.request('/?voidcache=true');
|
|
135
|
-
} catch { /* best-effort — don't block on failure */ }
|
|
132
|
+
// intentionally empty
|
|
136
133
|
}
|
|
137
134
|
|
|
138
135
|
/**
|
package/src/lib/config.js
CHANGED
|
@@ -9,6 +9,8 @@ const CREDENTIALS_FILE = 'credentials.json';
|
|
|
9
9
|
const COOKIES_FILE = 'cookies.txt';
|
|
10
10
|
const SYNCHRONIZE_FILE = 'synchronize.json';
|
|
11
11
|
const BASELINE_FILE = '.app_baseline.json';
|
|
12
|
+
const SCRIPTS_FILE = 'scripts.json';
|
|
13
|
+
const SCRIPTS_LOCAL_FILE = 'scripts.local.json';
|
|
12
14
|
|
|
13
15
|
function dboDir() {
|
|
14
16
|
return join(process.cwd(), DBO_DIR);
|
|
@@ -897,3 +899,40 @@ export async function loadExtensionDocumentationMDPlacement() {
|
|
|
897
899
|
return cfg.ExtensionDocumentationMDPlacement || null;
|
|
898
900
|
} catch { return null; }
|
|
899
901
|
}
|
|
902
|
+
|
|
903
|
+
// ─── Script Hooks (.dbo/scripts.json, .dbo/scripts.local.json) ────────────
|
|
904
|
+
|
|
905
|
+
function scriptsPath() {
|
|
906
|
+
return join(dboDir(), SCRIPTS_FILE);
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
function scriptsLocalPath() {
|
|
910
|
+
return join(dboDir(), SCRIPTS_LOCAL_FILE);
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
/**
|
|
914
|
+
* Load .dbo/scripts.json. Returns parsed object or null if missing.
|
|
915
|
+
* Throws SyntaxError with clear message if JSON is malformed.
|
|
916
|
+
*/
|
|
917
|
+
export async function loadScripts() {
|
|
918
|
+
const path = scriptsPath();
|
|
919
|
+
let raw;
|
|
920
|
+
try { raw = await readFile(path, 'utf8'); } catch { return null; }
|
|
921
|
+
try { return JSON.parse(raw); } catch (err) {
|
|
922
|
+
throw new SyntaxError(`Invalid JSON in .dbo/scripts.json: ${err.message}`);
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
/**
|
|
927
|
+
* Load .dbo/scripts.local.json (gitignored per-user overrides).
|
|
928
|
+
* Returns parsed object or null if missing.
|
|
929
|
+
* Throws SyntaxError with clear message if JSON is malformed.
|
|
930
|
+
*/
|
|
931
|
+
export async function loadScriptsLocal() {
|
|
932
|
+
const path = scriptsLocalPath();
|
|
933
|
+
let raw;
|
|
934
|
+
try { raw = await readFile(path, 'utf8'); } catch { return null; }
|
|
935
|
+
try { return JSON.parse(raw); } catch (err) {
|
|
936
|
+
throw new SyntaxError(`Invalid JSON in .dbo/scripts.local.json: ${err.message}`);
|
|
937
|
+
}
|
|
938
|
+
}
|
package/src/lib/delta.js
CHANGED
|
@@ -81,7 +81,13 @@ export async function compareFileContent(filePath, baselineValue) {
|
|
|
81
81
|
|
|
82
82
|
return normalizedCurrent !== normalizedBaseline;
|
|
83
83
|
} catch (err) {
|
|
84
|
-
|
|
84
|
+
if (err.code === 'ENOENT') {
|
|
85
|
+
// File doesn't exist on disk — nothing to push regardless of baseline.
|
|
86
|
+
// This commonly happens with CustomSQL companion files that clone
|
|
87
|
+
// didn't extract (e.g. non-CustomSQL type with server-side content).
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
// Other read errors — treat as changed to be safe
|
|
85
91
|
log.warn(`Failed to read ${filePath}: ${err.message}`);
|
|
86
92
|
return true;
|
|
87
93
|
}
|
package/src/lib/diff.js
CHANGED
|
@@ -155,7 +155,7 @@ export async function findMetadataFiles(dir, ig) {
|
|
|
155
155
|
const relPath = relative(process.cwd(), fullPath).replace(/\\/g, '/');
|
|
156
156
|
if (ig.ignores(relPath + '/') || ig.ignores(relPath)) continue;
|
|
157
157
|
results.push(...await findMetadataFiles(fullPath, ig));
|
|
158
|
-
} else if (entry.name.endsWith('.metadata.json')) {
|
|
158
|
+
} else if (entry.name.endsWith('.metadata.json') && !entry.name.startsWith('__WILL_DELETE__')) {
|
|
159
159
|
const relPath = relative(process.cwd(), fullPath).replace(/\\/g, '/');
|
|
160
160
|
if (!ig.ignores(relPath)) results.push(fullPath);
|
|
161
161
|
} else if (entry.name.endsWith('.json') && !entry.name.endsWith('.metadata.json') && !entry.name.includes('.CustomSQL.') && entry.name.includes('~')) {
|
|
@@ -217,6 +217,14 @@ export async function hasLocalModifications(metaPath, config = {}) {
|
|
|
217
217
|
const metaDir = dirname(metaPath);
|
|
218
218
|
const contentCols = meta._contentColumns || [];
|
|
219
219
|
|
|
220
|
+
// Load baseline for content comparison fallback (build tools can
|
|
221
|
+
// rewrite files with identical content, bumping mtime without any
|
|
222
|
+
// real change — e.g. sass recompile producing the same CSS).
|
|
223
|
+
const baseline = _cachedBaseline || await loadAppJsonBaseline();
|
|
224
|
+
const baselineEntry = baseline
|
|
225
|
+
? findBaselineEntry(baseline, meta._entity, meta.UID)
|
|
226
|
+
: null;
|
|
227
|
+
|
|
220
228
|
for (const col of contentCols) {
|
|
221
229
|
const ref = meta[col];
|
|
222
230
|
if (ref && String(ref).startsWith('@')) {
|
|
@@ -224,6 +232,20 @@ export async function hasLocalModifications(metaPath, config = {}) {
|
|
|
224
232
|
try {
|
|
225
233
|
const contentStat = await stat(contentPath);
|
|
226
234
|
if (contentStat.mtime.getTime() > syncTime + 2000) {
|
|
235
|
+
// mtime says modified — verify with content comparison against
|
|
236
|
+
// baseline to rule out false positives from build tools.
|
|
237
|
+
if (baselineEntry && baselineEntry[col] != null) {
|
|
238
|
+
try {
|
|
239
|
+
const localContent = await readFile(contentPath, 'utf8');
|
|
240
|
+
const normalizedLocal = localContent.replace(/\r\n/g, '\n').trimEnd();
|
|
241
|
+
const normalizedBaseline = String(baselineEntry[col]).replace(/\r\n/g, '\n').trimEnd();
|
|
242
|
+
if (normalizedLocal === normalizedBaseline) {
|
|
243
|
+
// Content identical — false positive mtime bump, fix timestamp
|
|
244
|
+
await setFileTimestamps(contentPath, meta._CreatedOn, meta._LastUpdated, serverTz);
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
} catch { /* read failed — treat as modified */ }
|
|
248
|
+
}
|
|
227
249
|
return true;
|
|
228
250
|
}
|
|
229
251
|
} catch { /* missing file */ }
|
|
@@ -687,7 +709,7 @@ function buildChangeMessage(recordName, serverRecord, config, options = {}) {
|
|
|
687
709
|
*/
|
|
688
710
|
function formatDateHint(serverDate, localDate) {
|
|
689
711
|
const fmt = (d) => d instanceof Date && !isNaN(d)
|
|
690
|
-
? d.toLocaleString('en-US', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' })
|
|
712
|
+
? d.toLocaleString('en-US', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit', second: '2-digit' })
|
|
691
713
|
: null;
|
|
692
714
|
const s = fmt(serverDate);
|
|
693
715
|
const l = fmt(localDate);
|
package/src/lib/filenames.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* All entity files use <basename>~<uid>.<ext> as the local filename.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { rename, writeFile } from 'fs/promises';
|
|
6
|
+
import { rename, unlink, writeFile, readFile, readdir } from 'fs/promises';
|
|
7
7
|
import { join, dirname, basename, extname } from 'path';
|
|
8
8
|
|
|
9
9
|
/**
|
|
@@ -20,6 +20,65 @@ export function buildUidFilename(name, uid, ext = '') {
|
|
|
20
20
|
return ext ? `${base}.${ext}` : base;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
/**
|
|
24
|
+
* Build the natural companion filename for a content record.
|
|
25
|
+
* No ~UID suffix is embedded. Implements Name/Path/Extension priority.
|
|
26
|
+
*
|
|
27
|
+
* Priority:
|
|
28
|
+
* 1. Name is null → last segment of Path (includes ext if present)
|
|
29
|
+
* 2. Name present + Extension present → name.ext (lowercased ext, double-ext stripped)
|
|
30
|
+
* 3. Name present + Extension null + Path last segment matches Name with ext → path segment
|
|
31
|
+
* 4. Name present + Extension null + no usable Path → just Name (no ext)
|
|
32
|
+
* 5. No Name and no Path → fallback (typically UID)
|
|
33
|
+
*
|
|
34
|
+
* @param {Object} record - Server record with Name, Path, Extension fields
|
|
35
|
+
* @param {string} fallback - Value when nothing else is available (e.g. record UID)
|
|
36
|
+
* @returns {string}
|
|
37
|
+
*/
|
|
38
|
+
export function buildContentFileName(record, fallback = 'untitled') {
|
|
39
|
+
const { Name, Path, Extension } = record;
|
|
40
|
+
|
|
41
|
+
// Priority 1: Name is null → use last segment of Path
|
|
42
|
+
if (!Name) {
|
|
43
|
+
if (Path) {
|
|
44
|
+
const segs = String(Path).replace(/\\/g, '/').split('/').filter(Boolean);
|
|
45
|
+
const last = segs[segs.length - 1];
|
|
46
|
+
if (last) return last;
|
|
47
|
+
}
|
|
48
|
+
return fallback;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const name = String(Name);
|
|
52
|
+
|
|
53
|
+
// Priority 2: Extension present → name.ext
|
|
54
|
+
if (Extension) {
|
|
55
|
+
const ext = String(Extension).toLowerCase();
|
|
56
|
+
const dotExt = `.${ext}`;
|
|
57
|
+
// Strip double extension (Name="colors.css", Extension="css" → "colors.css")
|
|
58
|
+
const nameClean = name.toLowerCase().endsWith(dotExt)
|
|
59
|
+
? name.slice(0, -dotExt.length)
|
|
60
|
+
: name;
|
|
61
|
+
return `${nameClean}.${ext}`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Priority 3: Extension null → check if Path last segment provides the extension
|
|
65
|
+
if (Path) {
|
|
66
|
+
const segs = String(Path).replace(/\\/g, '/').split('/').filter(Boolean);
|
|
67
|
+
const last = segs[segs.length - 1];
|
|
68
|
+
if (last) {
|
|
69
|
+
const lastExt = extname(last);
|
|
70
|
+
if (lastExt) {
|
|
71
|
+
const lastBase = basename(last, lastExt);
|
|
72
|
+
// If Path's last segment base matches Name, treat it as the canonical filename+ext
|
|
73
|
+
if (lastBase === name || last === name) return last;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Priority 4: No extension available → just name
|
|
79
|
+
return name;
|
|
80
|
+
}
|
|
81
|
+
|
|
23
82
|
/**
|
|
24
83
|
* Strip the ~<uid> portion from a local filename.
|
|
25
84
|
* Used when the local file is "logo~def456.png" but the upload should send "logo.png".
|
|
@@ -58,6 +117,55 @@ export function detectLegacyDotUid(filename) {
|
|
|
58
117
|
return match ? { name: match[1], uid: match[2] } : null;
|
|
59
118
|
}
|
|
60
119
|
|
|
120
|
+
/**
|
|
121
|
+
* Find the metadata file that references a given companion file via @reference.
|
|
122
|
+
*
|
|
123
|
+
* Lookup order:
|
|
124
|
+
* 1. Direct: base.metadata.json (natural name match)
|
|
125
|
+
* 2. Scan: all *.metadata.json in the same directory, check @reference values
|
|
126
|
+
*
|
|
127
|
+
* @param {string} companionPath - Absolute path to the companion file
|
|
128
|
+
* @returns {Promise<string|null>} - Absolute path to the metadata file, or null
|
|
129
|
+
*/
|
|
130
|
+
export async function findMetadataForCompanion(companionPath) {
|
|
131
|
+
const dir = dirname(companionPath);
|
|
132
|
+
const companionName = basename(companionPath);
|
|
133
|
+
const ext = extname(companionName);
|
|
134
|
+
const base = basename(companionName, ext);
|
|
135
|
+
|
|
136
|
+
// 1. Direct match: base.metadata.json
|
|
137
|
+
const directMeta = join(dir, `${base}.metadata.json`);
|
|
138
|
+
try {
|
|
139
|
+
await readFile(directMeta, 'utf8');
|
|
140
|
+
return directMeta;
|
|
141
|
+
} catch { /* not found */ }
|
|
142
|
+
|
|
143
|
+
// 2. Scan sibling *.metadata.json files for @reference match
|
|
144
|
+
let entries;
|
|
145
|
+
try { entries = await readdir(dir); } catch { return null; }
|
|
146
|
+
|
|
147
|
+
for (const entry of entries) {
|
|
148
|
+
if (!entry.endsWith('.metadata.json')) continue;
|
|
149
|
+
const metaPath = join(dir, entry);
|
|
150
|
+
try {
|
|
151
|
+
const meta = JSON.parse(await readFile(metaPath, 'utf8'));
|
|
152
|
+
const cols = [...(meta._contentColumns || [])];
|
|
153
|
+
if (meta._mediaFile) cols.push('_mediaFile');
|
|
154
|
+
for (const col of cols) {
|
|
155
|
+
const ref = meta[col];
|
|
156
|
+
if (!ref || !String(ref).startsWith('@')) continue;
|
|
157
|
+
const refName = String(ref).substring(1);
|
|
158
|
+
// Match both relative and root-relative (@/) references
|
|
159
|
+
if (refName === companionName || refName === `/${companionName}`) {
|
|
160
|
+
return metaPath;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
} catch { /* skip unreadable */ }
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
|
|
61
169
|
/**
|
|
62
170
|
* Rename a file pair (content + metadata) to the ~uid convention after server assigns a UID.
|
|
63
171
|
* Updates @reference values inside the metadata file.
|
|
@@ -75,68 +183,40 @@ export async function renameToUidConvention(meta, metaPath, uid, lastUpdated, se
|
|
|
75
183
|
const metaBase = basename(metaPath, '.metadata.json'); // e.g. "colors" or "logo.png"
|
|
76
184
|
|
|
77
185
|
if (hasUidInFilename(metaBase, uid)) {
|
|
78
|
-
return { newMetaPath: metaPath, newFilePath: null };
|
|
186
|
+
return { newMetaPath: metaPath, newFilePath: null, updatedMeta: meta };
|
|
79
187
|
}
|
|
80
188
|
|
|
189
|
+
// Compute new metadata filename with ~UID
|
|
81
190
|
// For media files: metaBase = "logo.png", newBase = "logo~uid.png"
|
|
82
191
|
// For content files: metaBase = "colors", newBase = "colors~uid"
|
|
83
192
|
const metaBaseExt = extname(metaBase); // ".png" for media metadata, "" for content metadata
|
|
84
193
|
let newMetaBase;
|
|
85
194
|
if (metaBaseExt) {
|
|
86
|
-
// Media: "logo.png" → "logo~uid.png"
|
|
87
195
|
const nameWithoutExt = metaBase.slice(0, -metaBaseExt.length);
|
|
88
196
|
newMetaBase = `${buildUidFilename(nameWithoutExt, uid)}${metaBaseExt}`;
|
|
89
197
|
} else {
|
|
90
|
-
// Content/entity-dir: "colors" → "colors~uid"
|
|
91
198
|
newMetaBase = buildUidFilename(metaBase, uid);
|
|
92
199
|
}
|
|
93
200
|
|
|
94
201
|
const newMetaPath = join(metaDir, `${newMetaBase}.metadata.json`);
|
|
95
202
|
|
|
96
|
-
// Update
|
|
97
|
-
meta
|
|
98
|
-
const updatedMeta = { ...meta };
|
|
99
|
-
|
|
100
|
-
// Rename content files referenced in _contentColumns and _mediaFile
|
|
101
|
-
const contentCols = [...(meta._contentColumns || [])];
|
|
102
|
-
if (meta._mediaFile) contentCols.push('_mediaFile');
|
|
103
|
-
|
|
104
|
-
let newFilePath = null;
|
|
105
|
-
|
|
106
|
-
for (const col of contentCols) {
|
|
107
|
-
const ref = meta[col];
|
|
108
|
-
if (!ref || !String(ref).startsWith('@')) continue;
|
|
109
|
-
|
|
110
|
-
const oldRefFile = String(ref).substring(1);
|
|
111
|
-
const oldFilePath = join(metaDir, oldRefFile);
|
|
112
|
-
|
|
113
|
-
// Compute new filename: insert uid
|
|
114
|
-
const oldExt = extname(oldRefFile); // ".css", ".png"
|
|
115
|
-
const oldBase = basename(oldRefFile, oldExt); // "colors" or "logo"
|
|
116
|
-
const newRefBase = buildUidFilename(oldBase, uid);
|
|
117
|
-
const newRefFile = oldExt ? `${newRefBase}${oldExt}` : newRefBase;
|
|
118
|
-
const newRefPath = join(metaDir, newRefFile);
|
|
119
|
-
|
|
120
|
-
try {
|
|
121
|
-
await rename(oldFilePath, newRefPath);
|
|
122
|
-
updatedMeta[col] = `@${newRefFile}`;
|
|
123
|
-
if (!newFilePath) newFilePath = newRefPath;
|
|
124
|
-
} catch {
|
|
125
|
-
// File may already be renamed or may not exist
|
|
126
|
-
}
|
|
127
|
-
}
|
|
203
|
+
// Update only the UID field; @reference values are UNCHANGED (companions keep natural names)
|
|
204
|
+
const updatedMeta = { ...meta, UID: uid };
|
|
128
205
|
|
|
129
206
|
// Write updated metadata to new path
|
|
130
207
|
await writeFile(newMetaPath, JSON.stringify(updatedMeta, null, 2) + '\n');
|
|
131
208
|
|
|
132
|
-
// Remove old metadata file
|
|
209
|
+
// Remove old metadata file (new content already written to newMetaPath above)
|
|
133
210
|
if (metaPath !== newMetaPath) {
|
|
134
|
-
try { await
|
|
211
|
+
try { await unlink(metaPath); } catch { /* old file already gone */ }
|
|
135
212
|
}
|
|
136
213
|
|
|
137
|
-
// Restore timestamps
|
|
214
|
+
// Restore timestamps for metadata file and companions (companions are not renamed)
|
|
138
215
|
if (serverTz && lastUpdated) {
|
|
139
216
|
const { setFileTimestamps } = await import('./timestamps.js');
|
|
217
|
+
try { await setFileTimestamps(newMetaPath, lastUpdated, lastUpdated, serverTz); } catch {}
|
|
218
|
+
const contentCols = [...(meta._contentColumns || [])];
|
|
219
|
+
if (meta._mediaFile) contentCols.push('_mediaFile');
|
|
140
220
|
for (const col of contentCols) {
|
|
141
221
|
const ref = updatedMeta[col];
|
|
142
222
|
if (ref && String(ref).startsWith('@')) {
|
|
@@ -144,8 +224,7 @@ export async function renameToUidConvention(meta, metaPath, uid, lastUpdated, se
|
|
|
144
224
|
try { await setFileTimestamps(fp, lastUpdated, lastUpdated, serverTz); } catch {}
|
|
145
225
|
}
|
|
146
226
|
}
|
|
147
|
-
try { await setFileTimestamps(newMetaPath, lastUpdated, lastUpdated, serverTz); } catch {}
|
|
148
227
|
}
|
|
149
228
|
|
|
150
|
-
return { newMetaPath, newFilePath, updatedMeta };
|
|
229
|
+
return { newMetaPath, newFilePath: null, updatedMeta };
|
|
151
230
|
}
|
package/src/lib/ignore.js
CHANGED
package/src/lib/input-parser.js
CHANGED
|
@@ -48,6 +48,15 @@ export async function buildInputBody(dataExpressions, extraParams = {}) {
|
|
|
48
48
|
return parts.join('&');
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
/**
|
|
52
|
+
* Encode a DBO input key for URL-encoded form data.
|
|
53
|
+
* Preserves : and ; which are structural separators in DBO syntax
|
|
54
|
+
* (e.g. "RowID:del123;entity:content"), but encodes everything else.
|
|
55
|
+
*/
|
|
56
|
+
function encodeDboKey(key) {
|
|
57
|
+
return encodeURIComponent(key).replace(/%3A/gi, ':').replace(/%3B/gi, ';');
|
|
58
|
+
}
|
|
59
|
+
|
|
51
60
|
async function encodeInputExpression(expr) {
|
|
52
61
|
// Check if value part contains @filepath
|
|
53
62
|
// Format: key=value or key@filepath
|
|
@@ -68,15 +77,15 @@ async function encodeInputExpression(expr) {
|
|
|
68
77
|
const key = expr.substring(0, atIdx);
|
|
69
78
|
const filePath = expr.substring(atIdx + 1);
|
|
70
79
|
const content = await readFile(filePath, 'utf8');
|
|
71
|
-
return `${
|
|
80
|
+
return `${encodeDboKey(key)}=${encodeURIComponent(content)}`;
|
|
72
81
|
} else if (eqIdx !== -1) {
|
|
73
82
|
// =value syntax: direct value
|
|
74
83
|
const key = expr.substring(0, eqIdx);
|
|
75
84
|
const value = expr.substring(eqIdx + 1);
|
|
76
|
-
return `${
|
|
85
|
+
return `${encodeDboKey(key)}=${encodeURIComponent(value)}`;
|
|
77
86
|
} else {
|
|
78
|
-
// No value separator, encode the whole thing
|
|
79
|
-
return
|
|
87
|
+
// No value separator, encode the whole thing (preserve DBO separators)
|
|
88
|
+
return encodeDboKey(expr);
|
|
80
89
|
}
|
|
81
90
|
}
|
|
82
91
|
|