@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
|
@@ -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
|
+
});
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { tagProjectFiles } from '../lib/tagging.js';
|
|
4
|
+
import { loadTagConfig, saveTagConfig } from '../lib/config.js';
|
|
5
|
+
import { loadIgnore } from '../lib/ignore.js';
|
|
6
|
+
import { join } from 'path';
|
|
7
|
+
|
|
8
|
+
export const tagCommand = new Command('tag')
|
|
9
|
+
.description('Apply sync-status tags (Finder color tags / gio emblems) to project files')
|
|
10
|
+
.argument('[path]', 'File or directory to tag (defaults to entire project)')
|
|
11
|
+
.option('--clear', 'Remove all dbo:* tags from companion files')
|
|
12
|
+
.option('--status', 'Show counts of synced / modified / untracked / trashed files')
|
|
13
|
+
.option('--enable', 'Enable automatic tagging after clone/pull/push')
|
|
14
|
+
.option('--disable', 'Disable automatic tagging after clone/pull/push')
|
|
15
|
+
.option('--verbose', 'Log each file with its status')
|
|
16
|
+
.action(async (pathArg, options) => {
|
|
17
|
+
if (options.enable) {
|
|
18
|
+
await saveTagConfig(true);
|
|
19
|
+
console.log(chalk.green('✔ Automatic file tagging enabled'));
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
if (options.disable) {
|
|
23
|
+
await saveTagConfig(false);
|
|
24
|
+
console.log(chalk.yellow('Automatic file tagging disabled'));
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Verify project is initialized
|
|
29
|
+
const ig = await loadIgnore().catch(() => null);
|
|
30
|
+
if (!ig) {
|
|
31
|
+
console.error(chalk.red('Not a dbo project'));
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const dir = pathArg ? join(process.cwd(), pathArg) : process.cwd();
|
|
36
|
+
|
|
37
|
+
if (options.clear) {
|
|
38
|
+
await tagProjectFiles({ clearAll: true, dir, verbose: options.verbose });
|
|
39
|
+
console.log(chalk.green('✔ All dbo:* tags cleared'));
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const counts = await tagProjectFiles({ verbose: options.verbose, dir });
|
|
44
|
+
|
|
45
|
+
if (counts === null) {
|
|
46
|
+
const { tagFiles } = await loadTagConfig();
|
|
47
|
+
if (!tagFiles) {
|
|
48
|
+
console.log(chalk.yellow('File tagging is disabled. Run `dbo tag --enable` to enable.'));
|
|
49
|
+
} else {
|
|
50
|
+
console.log(chalk.dim('File tagging is not supported on this platform.'));
|
|
51
|
+
}
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (options.status || options.verbose) {
|
|
56
|
+
console.log(
|
|
57
|
+
`${chalk.green(counts.synced)} synced, ` +
|
|
58
|
+
`${chalk.blue(counts.modified)} modified, ` +
|
|
59
|
+
`${chalk.yellow(counts.untracked)} untracked, ` +
|
|
60
|
+
`${chalk.red(counts.trashed)} trashed`
|
|
61
|
+
);
|
|
62
|
+
} else {
|
|
63
|
+
console.log(chalk.green(`✔ Tags applied (${counts.synced} synced, ${counts.modified} modified, ${counts.untracked} untracked)`));
|
|
64
|
+
}
|
|
65
|
+
});
|
package/src/lib/config.js
CHANGED
|
@@ -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,68 @@ 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
|
+
}
|
|
939
|
+
|
|
940
|
+
// ─── Tag Config ───────────────────────────────────────────────────────────────
|
|
941
|
+
|
|
942
|
+
/**
|
|
943
|
+
* Load the tagFiles setting from config.json.
|
|
944
|
+
* @returns {Promise<{tagFiles: boolean}>} defaults to true
|
|
945
|
+
*/
|
|
946
|
+
export async function loadTagConfig() {
|
|
947
|
+
try {
|
|
948
|
+
const raw = await readFile(join(process.cwd(), DBO_DIR, CONFIG_FILE), 'utf8');
|
|
949
|
+
const config = JSON.parse(raw);
|
|
950
|
+
return { tagFiles: config.tagFiles !== false };
|
|
951
|
+
} catch {
|
|
952
|
+
return { tagFiles: true };
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
/**
|
|
957
|
+
* Enable or disable automatic file tagging by writing to config.json.
|
|
958
|
+
* @param {boolean} enabled
|
|
959
|
+
*/
|
|
960
|
+
export async function saveTagConfig(enabled) {
|
|
961
|
+
const configPath = join(process.cwd(), DBO_DIR, CONFIG_FILE);
|
|
962
|
+
let config = {};
|
|
963
|
+
try { config = JSON.parse(await readFile(configPath, 'utf8')); } catch {}
|
|
964
|
+
config.tagFiles = enabled;
|
|
965
|
+
await writeFile(configPath, JSON.stringify(config, null, 2));
|
|
966
|
+
}
|
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
|
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { readFile, writeFile, mkdir } from 'fs/promises';
|
|
2
|
+
import { join, relative, extname, basename } from 'path';
|
|
3
|
+
import { log } from './logger.js';
|
|
4
|
+
|
|
5
|
+
const DEPLOY_CONFIG_FILE = '.dbo/deploy_config.json';
|
|
6
|
+
|
|
7
|
+
function deployConfigPath() {
|
|
8
|
+
return join(process.cwd(), DEPLOY_CONFIG_FILE);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function sortedKeys(obj) {
|
|
12
|
+
return Object.fromEntries(Object.entries(obj).sort(([a], [b]) => a.localeCompare(b)));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Load .dbo/deploy_config.json. Returns { deployments: {} } if missing.
|
|
17
|
+
* Throws on malformed JSON (do not silently recreate — would lose existing entries).
|
|
18
|
+
*/
|
|
19
|
+
export async function loadDeployConfig() {
|
|
20
|
+
try {
|
|
21
|
+
const raw = await readFile(deployConfigPath(), 'utf8');
|
|
22
|
+
return JSON.parse(raw);
|
|
23
|
+
} catch (err) {
|
|
24
|
+
if (err.code === 'ENOENT') return { deployments: {} };
|
|
25
|
+
throw new Error(`Failed to parse ${DEPLOY_CONFIG_FILE}: ${err.message}`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Write .dbo/deploy_config.json with alphabetically sorted deployment keys.
|
|
31
|
+
*/
|
|
32
|
+
export async function saveDeployConfig(config) {
|
|
33
|
+
await mkdir(join(process.cwd(), '.dbo'), { recursive: true });
|
|
34
|
+
const sorted = { ...config, deployments: sortedKeys(config.deployments || {}) };
|
|
35
|
+
await writeFile(deployConfigPath(), JSON.stringify(sorted, null, 2) + '\n');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Derive the <ext>:<basename> deploy key from a relative file path.
|
|
40
|
+
* Finds the shortest key not already occupied in existingDeployments.
|
|
41
|
+
*
|
|
42
|
+
* @param {string} relPath - e.g. "lib/bins/app/assets/css/colors.css"
|
|
43
|
+
* @param {Object} existingDeployments - current deployments object (to avoid collisions)
|
|
44
|
+
* @returns {string} - e.g. "css:colors"
|
|
45
|
+
*/
|
|
46
|
+
export function buildDeployKey(relPath, existingDeployments = {}) {
|
|
47
|
+
const normalized = relPath.replace(/\\/g, '/');
|
|
48
|
+
const parts = normalized.split('/');
|
|
49
|
+
const filename = parts[parts.length - 1];
|
|
50
|
+
const ext = extname(filename).slice(1).toLowerCase(); // '' if no dot
|
|
51
|
+
const base = ext ? basename(filename, `.${ext}`) : filename;
|
|
52
|
+
const prefix = ext || 'file';
|
|
53
|
+
|
|
54
|
+
// Try simplest key first, then progressively add parent segments for disambiguation
|
|
55
|
+
const dirParts = parts.slice(0, -1);
|
|
56
|
+
const candidates = [`${prefix}:${base}`];
|
|
57
|
+
for (let depth = 1; depth <= dirParts.length; depth++) {
|
|
58
|
+
const suffix = dirParts.slice(-depth).join('/');
|
|
59
|
+
candidates.push(`${prefix}:${suffix}/${base}`);
|
|
60
|
+
}
|
|
61
|
+
candidates.push(`${prefix}:${normalized}`); // absolute fallback
|
|
62
|
+
|
|
63
|
+
for (const key of candidates) {
|
|
64
|
+
if (!existingDeployments[key]) return key;
|
|
65
|
+
}
|
|
66
|
+
return `${prefix}:${normalized}`; // always unique
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Insert or update a deploy config entry for a companion file.
|
|
71
|
+
*
|
|
72
|
+
* Behaviour:
|
|
73
|
+
* - If an entry with the same UID already exists → update file, entity, column in place
|
|
74
|
+
* (keeps the existing key, avoids duplicate keys on re-clone)
|
|
75
|
+
* - If a new key collision exists with a DIFFERENT UID → log a warning, skip
|
|
76
|
+
* - Otherwise → create new entry with the shortest unique key
|
|
77
|
+
*
|
|
78
|
+
* @param {string} companionPath - Absolute path to the companion file
|
|
79
|
+
* @param {string} uid - Record UID (from metadata)
|
|
80
|
+
* @param {string} entity - Entity type (e.g. 'content', 'extension', 'media')
|
|
81
|
+
* @param {string} [column] - Column name (e.g. 'Content', 'CSS')
|
|
82
|
+
*/
|
|
83
|
+
export async function upsertDeployEntry(companionPath, uid, entity, column) {
|
|
84
|
+
if (!uid || !companionPath) return;
|
|
85
|
+
const config = await loadDeployConfig();
|
|
86
|
+
const { deployments } = config;
|
|
87
|
+
|
|
88
|
+
const relPath = relative(process.cwd(), companionPath).replace(/\\/g, '/');
|
|
89
|
+
|
|
90
|
+
// Find if this UID is already tracked under any key
|
|
91
|
+
const existingKey = Object.keys(deployments).find(k => deployments[k].uid === uid);
|
|
92
|
+
|
|
93
|
+
if (existingKey) {
|
|
94
|
+
// Update path + metadata in place — keeps existing key, avoids duplicates on re-clone
|
|
95
|
+
deployments[existingKey] = {
|
|
96
|
+
...deployments[existingKey],
|
|
97
|
+
file: relPath,
|
|
98
|
+
entity,
|
|
99
|
+
...(column ? { column } : {}),
|
|
100
|
+
};
|
|
101
|
+
config.deployments = sortedKeys(deployments);
|
|
102
|
+
await saveDeployConfig(config);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// New entry — build unique key avoiding all occupied keys
|
|
107
|
+
const key = buildDeployKey(relPath, deployments);
|
|
108
|
+
|
|
109
|
+
if (deployments[key]) {
|
|
110
|
+
// buildDeployKey returned an occupied key (should not happen — means all path segments collide)
|
|
111
|
+
log.warn(`Deploy config: skipping auto-registration of "${relPath}" — key "${key}" already exists for a different record. Add manually to ${DEPLOY_CONFIG_FILE}.`);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const entry = { uid, file: relPath, entity };
|
|
116
|
+
if (column) entry.column = column;
|
|
117
|
+
deployments[key] = entry;
|
|
118
|
+
config.deployments = sortedKeys(deployments);
|
|
119
|
+
await saveDeployConfig(config);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Remove the deploy config entry matching a given UID.
|
|
124
|
+
* No-op if no entry with that UID exists.
|
|
125
|
+
*
|
|
126
|
+
* @param {string} uid - Record UID to remove
|
|
127
|
+
*/
|
|
128
|
+
export async function removeDeployEntry(uid) {
|
|
129
|
+
if (!uid) return;
|
|
130
|
+
const config = await loadDeployConfig();
|
|
131
|
+
const { deployments } = config;
|
|
132
|
+
const key = Object.keys(deployments).find(k => deployments[k].uid === uid);
|
|
133
|
+
if (!key) return;
|
|
134
|
+
delete deployments[key];
|
|
135
|
+
config.deployments = sortedKeys(deployments);
|
|
136
|
+
await saveDeployConfig(config);
|
|
137
|
+
}
|
package/src/lib/diff.js
CHANGED
|
@@ -5,6 +5,7 @@ import { loadIgnore } from './ignore.js';
|
|
|
5
5
|
import { parseServerDate, setFileTimestamps } from './timestamps.js';
|
|
6
6
|
import { loadConfig, loadUserInfo, loadAppJsonBaseline } from './config.js';
|
|
7
7
|
import { findBaselineEntry } from './delta.js';
|
|
8
|
+
import { isMetadataFile, parseMetaFilename } from './filenames.js';
|
|
8
9
|
import { log } from './logger.js';
|
|
9
10
|
|
|
10
11
|
// ─── Baseline Cache ─────────────────────────────────────────────────────────
|
|
@@ -155,10 +156,10 @@ export async function findMetadataFiles(dir, ig) {
|
|
|
155
156
|
const relPath = relative(process.cwd(), fullPath).replace(/\\/g, '/');
|
|
156
157
|
if (ig.ignores(relPath + '/') || ig.ignores(relPath)) continue;
|
|
157
158
|
results.push(...await findMetadataFiles(fullPath, ig));
|
|
158
|
-
} else if (entry.name.
|
|
159
|
+
} else if (isMetadataFile(entry.name) && !entry.name.startsWith('__WILL_DELETE__')) {
|
|
159
160
|
const relPath = relative(process.cwd(), fullPath).replace(/\\/g, '/');
|
|
160
161
|
if (!ig.ignores(relPath)) results.push(fullPath);
|
|
161
|
-
} else if (entry.name.endsWith('.json') && !entry.name
|
|
162
|
+
} else if (entry.name.endsWith('.json') && !isMetadataFile(entry.name) && !entry.name.includes('.CustomSQL.') && entry.name.includes('~')) {
|
|
162
163
|
// Output hierarchy root files: Name~UID.json (or legacy _output~Name~UID.json)
|
|
163
164
|
// Exclude old-format child output files — they contain a dot-prefixed
|
|
164
165
|
// child-type segment (.column~, .join~, .filter~) before .json.
|
|
@@ -217,6 +218,14 @@ export async function hasLocalModifications(metaPath, config = {}) {
|
|
|
217
218
|
const metaDir = dirname(metaPath);
|
|
218
219
|
const contentCols = meta._contentColumns || [];
|
|
219
220
|
|
|
221
|
+
// Load baseline for content comparison fallback (build tools can
|
|
222
|
+
// rewrite files with identical content, bumping mtime without any
|
|
223
|
+
// real change — e.g. sass recompile producing the same CSS).
|
|
224
|
+
const baseline = _cachedBaseline || await loadAppJsonBaseline();
|
|
225
|
+
const baselineEntry = baseline
|
|
226
|
+
? findBaselineEntry(baseline, meta._entity, meta.UID)
|
|
227
|
+
: null;
|
|
228
|
+
|
|
220
229
|
for (const col of contentCols) {
|
|
221
230
|
const ref = meta[col];
|
|
222
231
|
if (ref && String(ref).startsWith('@')) {
|
|
@@ -224,6 +233,20 @@ export async function hasLocalModifications(metaPath, config = {}) {
|
|
|
224
233
|
try {
|
|
225
234
|
const contentStat = await stat(contentPath);
|
|
226
235
|
if (contentStat.mtime.getTime() > syncTime + 2000) {
|
|
236
|
+
// mtime says modified — verify with content comparison against
|
|
237
|
+
// baseline to rule out false positives from build tools.
|
|
238
|
+
if (baselineEntry && baselineEntry[col] != null) {
|
|
239
|
+
try {
|
|
240
|
+
const localContent = await readFile(contentPath, 'utf8');
|
|
241
|
+
const normalizedLocal = localContent.replace(/\r\n/g, '\n').trimEnd();
|
|
242
|
+
const normalizedBaseline = String(baselineEntry[col]).replace(/\r\n/g, '\n').trimEnd();
|
|
243
|
+
if (normalizedLocal === normalizedBaseline) {
|
|
244
|
+
// Content identical — false positive mtime bump, fix timestamp
|
|
245
|
+
await setFileTimestamps(contentPath, meta._CreatedOn, meta._LastUpdated, serverTz);
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
} catch { /* read failed — treat as modified */ }
|
|
249
|
+
}
|
|
227
250
|
return true;
|
|
228
251
|
}
|
|
229
252
|
} catch { /* missing file */ }
|
|
@@ -473,7 +496,7 @@ export async function compareRecord(metaPath, config, serverRecordsMap) {
|
|
|
473
496
|
}
|
|
474
497
|
|
|
475
498
|
const metaDir = dirname(metaPath);
|
|
476
|
-
const metaBase = basename(metaPath, '.metadata.json');
|
|
499
|
+
const metaBase = parseMetaFilename(basename(metaPath))?.naturalBase ?? basename(metaPath, '.metadata.json');
|
|
477
500
|
const contentCols = localMeta._contentColumns || [];
|
|
478
501
|
const fieldDiffs = [];
|
|
479
502
|
|
|
@@ -687,7 +710,7 @@ function buildChangeMessage(recordName, serverRecord, config, options = {}) {
|
|
|
687
710
|
*/
|
|
688
711
|
function formatDateHint(serverDate, localDate) {
|
|
689
712
|
const fmt = (d) => d instanceof Date && !isNaN(d)
|
|
690
|
-
? d.toLocaleString('en-US', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' })
|
|
713
|
+
? d.toLocaleString('en-US', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit', second: '2-digit' })
|
|
691
714
|
: null;
|
|
692
715
|
const s = fmt(serverDate);
|
|
693
716
|
const l = fmt(localDate);
|
|
@@ -777,7 +800,7 @@ export async function inlineDiffAndMerge(serverRow, metaPath, config, options =
|
|
|
777
800
|
}
|
|
778
801
|
|
|
779
802
|
const metaDir = dirname(metaPath);
|
|
780
|
-
const metaBase = basename(metaPath, '.metadata.json');
|
|
803
|
+
const metaBase = parseMetaFilename(basename(metaPath))?.naturalBase ?? basename(metaPath, '.metadata.json');
|
|
781
804
|
const contentCols = localMeta._contentColumns || [];
|
|
782
805
|
const fieldDiffs = [];
|
|
783
806
|
|