@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.
Files changed (50) hide show
  1. package/README.md +126 -3
  2. package/bin/dbo.js +4 -0
  3. package/package.json +1 -1
  4. package/plugins/claude/dbo/.claude-plugin/plugin.json +1 -1
  5. package/plugins/claude/dbo/commands/dbo.md +65 -244
  6. package/plugins/claude/dbo/docs/_audit_required/API/all.md +40 -0
  7. package/plugins/claude/dbo/docs/_audit_required/API/app.md +38 -0
  8. package/plugins/claude/dbo/docs/_audit_required/API/athenticate.md +26 -0
  9. package/plugins/claude/dbo/docs/_audit_required/API/cache.md +29 -0
  10. package/plugins/claude/dbo/docs/_audit_required/API/content.md +14 -0
  11. package/plugins/claude/dbo/docs/_audit_required/API/data_source.md +28 -0
  12. package/plugins/claude/dbo/docs/_audit_required/API/email.md +18 -0
  13. package/plugins/claude/dbo/docs/_audit_required/API/input.md +25 -0
  14. package/plugins/claude/dbo/docs/_audit_required/API/instance.md +28 -0
  15. package/plugins/claude/dbo/docs/_audit_required/API/log.md +8 -0
  16. package/plugins/claude/dbo/docs/_audit_required/API/media.md +12 -0
  17. package/plugins/claude/dbo/docs/_audit_required/API/output_by_entity.md +12 -0
  18. package/plugins/claude/dbo/docs/_audit_required/API/upload.md +7 -0
  19. package/plugins/claude/dbo/docs/_audit_required/dbo-api-syntax.md +1487 -0
  20. package/plugins/claude/dbo/docs/_audit_required/dbo-problems-code.md +111 -0
  21. package/plugins/claude/dbo/docs/_audit_required/dbo-problems-performance.md +109 -0
  22. package/plugins/claude/dbo/docs/_audit_required/dbo-problems-syntax.md +97 -0
  23. package/plugins/claude/dbo/docs/_audit_required/dbo-product-market.md +119 -0
  24. package/plugins/claude/dbo/docs/_audit_required/dbo-white-paper.md +125 -0
  25. package/plugins/claude/dbo/docs/dbo-cheat-sheet.md +323 -0
  26. package/plugins/claude/dbo/docs/dbo-cli-readme.md +2222 -0
  27. package/plugins/claude/dbo/docs/dbo-core-entities.md +878 -0
  28. package/plugins/claude/dbo/docs/dbo-output-customsql.md +677 -0
  29. package/plugins/claude/dbo/docs/dbo-output-query.md +967 -0
  30. package/plugins/claude/dbo/skills/cli/SKILL.md +62 -246
  31. package/src/commands/add.js +366 -62
  32. package/src/commands/build.js +102 -0
  33. package/src/commands/clone.js +602 -139
  34. package/src/commands/diff.js +4 -0
  35. package/src/commands/init.js +16 -2
  36. package/src/commands/input.js +3 -1
  37. package/src/commands/mv.js +12 -4
  38. package/src/commands/push.js +265 -70
  39. package/src/commands/rm.js +16 -3
  40. package/src/commands/run.js +81 -0
  41. package/src/lib/client.js +4 -7
  42. package/src/lib/config.js +39 -0
  43. package/src/lib/delta.js +7 -1
  44. package/src/lib/diff.js +24 -2
  45. package/src/lib/filenames.js +120 -41
  46. package/src/lib/ignore.js +6 -0
  47. package/src/lib/input-parser.js +13 -4
  48. package/src/lib/scripts.js +232 -0
  49. package/src/lib/toe-stepping.js +17 -2
  50. 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
- * Clear the server-side cache so that subsequent GET requests return fresh data.
128
- * Called before comparison fetches (toe-stepping, diff) and after POST submissions.
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
- try {
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
- // If file doesn't exist or can't be read, consider it changed
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);
@@ -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 }; // already renamed
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 metadata in memory
97
- meta.UID = uid;
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 if it's different from newMetaPath
209
+ // Remove old metadata file (new content already written to newMetaPath above)
133
210
  if (metaPath !== newMetaPath) {
134
- try { await rename(metaPath, newMetaPath); } catch { /* already written above */ }
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
@@ -44,6 +44,12 @@ package-lock.json
44
44
  src/
45
45
  test/
46
46
 
47
+ # Scaffold directories (managed by server, not user content)
48
+ trash/
49
+
50
+ # Build artifacts
51
+ *.map
52
+
47
53
  # Documentation (repo scaffolding)
48
54
  SETUP.md
49
55
  README.md
@@ -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 `${encodeURIComponent(key)}=${encodeURIComponent(content)}`;
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 `${encodeURIComponent(key)}=${encodeURIComponent(value)}`;
85
+ return `${encodeDboKey(key)}=${encodeURIComponent(value)}`;
77
86
  } else {
78
- // No value separator, encode the whole thing
79
- return encodeURIComponent(expr);
87
+ // No value separator, encode the whole thing (preserve DBO separators)
88
+ return encodeDboKey(expr);
80
89
  }
81
90
  }
82
91