@dboio/cli 0.4.2 → 0.6.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 +294 -71
- package/bin/dbo.js +12 -3
- package/bin/postinstall.js +88 -0
- package/package.json +10 -3
- package/src/commands/clone.js +597 -19
- package/src/commands/diff.js +246 -0
- package/src/commands/init.js +30 -22
- package/src/commands/install.js +517 -69
- package/src/commands/mv.js +869 -0
- package/src/commands/pull.js +6 -0
- package/src/commands/push.js +289 -33
- package/src/commands/rm.js +337 -0
- package/src/commands/status.js +28 -1
- package/src/lib/config.js +265 -0
- package/src/lib/delta.js +204 -0
- package/src/lib/dependencies.js +131 -0
- package/src/lib/diff.js +740 -0
- package/src/lib/save-to-disk.js +71 -4
- package/src/lib/structure.js +36 -0
- package/src/plugins/claudecommands/dbo.md +37 -6
- package/src/commands/update.js +0 -168
package/src/lib/delta.js
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { readFile } from 'fs/promises';
|
|
2
|
+
import { join, dirname } from 'path';
|
|
3
|
+
import { log } from './logger.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Load the baseline file (.app.json) from disk.
|
|
7
|
+
*
|
|
8
|
+
* @param {string} cwd - Current working directory
|
|
9
|
+
* @returns {Promise<Object|null>} - Baseline JSON or null if not found
|
|
10
|
+
*/
|
|
11
|
+
export async function loadBaseline(cwd = process.cwd()) {
|
|
12
|
+
const baselinePath = join(cwd, '.app.json');
|
|
13
|
+
try {
|
|
14
|
+
const raw = await readFile(baselinePath, 'utf8');
|
|
15
|
+
return JSON.parse(raw);
|
|
16
|
+
} catch (err) {
|
|
17
|
+
if (err.code === 'ENOENT') {
|
|
18
|
+
return null; // Baseline doesn't exist
|
|
19
|
+
}
|
|
20
|
+
log.warn(`Failed to parse .app.json: ${err.message}`);
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Save baseline data to .app.json file.
|
|
27
|
+
*
|
|
28
|
+
* @param {Object} data - Baseline JSON data
|
|
29
|
+
* @param {string} cwd - Current working directory
|
|
30
|
+
*/
|
|
31
|
+
export async function saveBaseline(data, cwd = process.cwd()) {
|
|
32
|
+
const { writeFile } = await import('fs/promises');
|
|
33
|
+
const baselinePath = join(cwd, '.app.json');
|
|
34
|
+
await writeFile(baselinePath, JSON.stringify(data, null, 2), 'utf8');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Find a baseline entry by UID and entity type.
|
|
39
|
+
* Traverses the baseline's children hierarchy.
|
|
40
|
+
*
|
|
41
|
+
* @param {Object} baseline - The baseline JSON
|
|
42
|
+
* @param {string} entity - Entity type (e.g., "content", "output")
|
|
43
|
+
* @param {string} uid - Record UID
|
|
44
|
+
* @returns {Object|null} - Matching entry or null
|
|
45
|
+
*/
|
|
46
|
+
export function findBaselineEntry(baseline, entity, uid) {
|
|
47
|
+
if (!baseline || !baseline.children) {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const entityArray = baseline.children[entity];
|
|
52
|
+
if (!Array.isArray(entityArray)) {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return entityArray.find(item => item.UID === uid) || null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Compare a file's content on disk against a baseline value.
|
|
61
|
+
*
|
|
62
|
+
* @param {string} filePath - Absolute path to file
|
|
63
|
+
* @param {string|null} baselineValue - Baseline value to compare against
|
|
64
|
+
* @returns {Promise<boolean>} - True if different, false if same
|
|
65
|
+
*/
|
|
66
|
+
export async function compareFileContent(filePath, baselineValue) {
|
|
67
|
+
try {
|
|
68
|
+
const currentContent = await readFile(filePath, 'utf8');
|
|
69
|
+
|
|
70
|
+
// Normalize line endings for comparison
|
|
71
|
+
const normalizedCurrent = currentContent.replace(/\r\n/g, '\n').trim();
|
|
72
|
+
const normalizedBaseline = (baselineValue || '').replace(/\r\n/g, '\n').trim();
|
|
73
|
+
|
|
74
|
+
return normalizedCurrent !== normalizedBaseline;
|
|
75
|
+
} catch (err) {
|
|
76
|
+
// If file doesn't exist or can't be read, consider it changed
|
|
77
|
+
log.warn(`Failed to read ${filePath}: ${err.message}`);
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Detect which columns have changed by comparing current metadata against baseline.
|
|
84
|
+
*
|
|
85
|
+
* @param {string} metaPath - Path to metadata.json file
|
|
86
|
+
* @param {Object} baseline - The baseline JSON
|
|
87
|
+
* @param {Object} config - CLI config (for resolving file paths)
|
|
88
|
+
* @returns {Promise<string[]>} - Array of changed column names
|
|
89
|
+
*/
|
|
90
|
+
export async function detectChangedColumns(metaPath, baseline, config) {
|
|
91
|
+
// Load current metadata
|
|
92
|
+
const metaRaw = await readFile(metaPath, 'utf8');
|
|
93
|
+
const metadata = JSON.parse(metaRaw);
|
|
94
|
+
|
|
95
|
+
const { UID, _entity } = metadata;
|
|
96
|
+
|
|
97
|
+
// Find matching baseline entry
|
|
98
|
+
const baselineEntry = findBaselineEntry(baseline, _entity, UID);
|
|
99
|
+
|
|
100
|
+
// If no baseline exists for this record, all non-system columns are "changed"
|
|
101
|
+
if (!baselineEntry) {
|
|
102
|
+
return getAllUserColumns(metadata);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const changedColumns = [];
|
|
106
|
+
const metaDir = dirname(metaPath);
|
|
107
|
+
|
|
108
|
+
// Compare each column
|
|
109
|
+
for (const [columnName, columnValue] of Object.entries(metadata)) {
|
|
110
|
+
// Skip system columns and special fields
|
|
111
|
+
if (shouldSkipColumn(columnName)) {
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Check if value is a @reference
|
|
116
|
+
if (isReference(columnValue)) {
|
|
117
|
+
const refPath = resolveReferencePath(columnValue, metaDir);
|
|
118
|
+
const baselineValue = baselineEntry[columnName];
|
|
119
|
+
|
|
120
|
+
// Compare file content
|
|
121
|
+
const isDifferent = await compareFileContent(refPath, baselineValue);
|
|
122
|
+
if (isDifferent) {
|
|
123
|
+
changedColumns.push(columnName);
|
|
124
|
+
}
|
|
125
|
+
} else {
|
|
126
|
+
// Scalar value comparison
|
|
127
|
+
const currentValue = normalizeValue(columnValue);
|
|
128
|
+
const baselineValue = normalizeValue(baselineEntry[columnName]);
|
|
129
|
+
|
|
130
|
+
if (currentValue !== baselineValue) {
|
|
131
|
+
changedColumns.push(columnName);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return changedColumns;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Get all user-defined columns (non-system columns).
|
|
141
|
+
*
|
|
142
|
+
* @param {Object} metadata - Metadata object
|
|
143
|
+
* @returns {string[]} - Array of user column names
|
|
144
|
+
*/
|
|
145
|
+
function getAllUserColumns(metadata) {
|
|
146
|
+
return Object.keys(metadata).filter(col => !shouldSkipColumn(col));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Determine if a column should be skipped (system columns).
|
|
151
|
+
*
|
|
152
|
+
* @param {string} columnName - Column name
|
|
153
|
+
* @returns {boolean} - True if should skip
|
|
154
|
+
*/
|
|
155
|
+
function shouldSkipColumn(columnName) {
|
|
156
|
+
// Skip system columns starting with underscore, UID, and children
|
|
157
|
+
return columnName.startsWith('_') ||
|
|
158
|
+
columnName === 'UID' ||
|
|
159
|
+
columnName === 'children';
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Check if a value is a @reference object.
|
|
164
|
+
*
|
|
165
|
+
* @param {*} value - Value to check
|
|
166
|
+
* @returns {boolean} - True if reference
|
|
167
|
+
*/
|
|
168
|
+
function isReference(value) {
|
|
169
|
+
return value &&
|
|
170
|
+
typeof value === 'object' &&
|
|
171
|
+
!Array.isArray(value) &&
|
|
172
|
+
value['@reference'] !== undefined;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Resolve a @reference path to absolute file path.
|
|
177
|
+
*
|
|
178
|
+
* @param {Object} reference - Reference object with @reference property
|
|
179
|
+
* @param {string} baseDir - Base directory containing metadata
|
|
180
|
+
* @returns {string} - Absolute file path
|
|
181
|
+
*/
|
|
182
|
+
function resolveReferencePath(reference, baseDir) {
|
|
183
|
+
const refPath = reference['@reference'];
|
|
184
|
+
return join(baseDir, refPath);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Normalize a value for comparison (handle null, undefined, etc.).
|
|
189
|
+
*
|
|
190
|
+
* @param {*} value - Value to normalize
|
|
191
|
+
* @returns {string} - Normalized string value
|
|
192
|
+
*/
|
|
193
|
+
function normalizeValue(value) {
|
|
194
|
+
if (value === null || value === undefined) {
|
|
195
|
+
return '';
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// If it's still a @reference object in baseline, compare as string
|
|
199
|
+
if (isReference(value)) {
|
|
200
|
+
return JSON.stringify(value);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return String(value).trim();
|
|
204
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dependency management for entity synchronization.
|
|
3
|
+
* Ensures children are processed before parents to maintain referential integrity.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Entity dependency hierarchy.
|
|
8
|
+
* Lower levels must be processed before higher levels.
|
|
9
|
+
* This ensures foreign key relationships are maintained (children before parents).
|
|
10
|
+
*/
|
|
11
|
+
export const ENTITY_DEPENDENCIES = {
|
|
12
|
+
// Level 1: Most dependent (children)
|
|
13
|
+
'output_value_filter': 1,
|
|
14
|
+
'output_value_entity_column_rel': 2,
|
|
15
|
+
|
|
16
|
+
// Level 2: Mid-level dependencies
|
|
17
|
+
'output_value': 2,
|
|
18
|
+
|
|
19
|
+
// Level 3: Parent entities
|
|
20
|
+
'output': 3,
|
|
21
|
+
|
|
22
|
+
// Default level 0 for entities not in this map
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Get the dependency level for an entity type.
|
|
27
|
+
*
|
|
28
|
+
* @param {string} entity - Entity type name
|
|
29
|
+
* @returns {number} - Dependency level (0 = no dependencies, higher = more dependent)
|
|
30
|
+
*/
|
|
31
|
+
function getDependencyLevel(entity) {
|
|
32
|
+
return ENTITY_DEPENDENCIES[entity] || 0;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Build a dependency-ordered structure from synchronize.json data.
|
|
37
|
+
* Returns operations grouped by type and sorted by dependency level.
|
|
38
|
+
*
|
|
39
|
+
* @param {Object} synchronizeData - The synchronize.json contents
|
|
40
|
+
* @returns {Object} - Ordered structure: { delete: [], add: [], edit: [] }
|
|
41
|
+
*/
|
|
42
|
+
export function buildDependencyGraph(synchronizeData) {
|
|
43
|
+
if (!synchronizeData) {
|
|
44
|
+
return { delete: [], add: [], edit: [] };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const result = {
|
|
48
|
+
delete: sortByDependency(synchronizeData.delete || [], 'delete'),
|
|
49
|
+
add: sortByDependency(synchronizeData.add || [], 'add'),
|
|
50
|
+
edit: sortByDependency(synchronizeData.edit || [], 'edit'),
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
return result;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Sort entries by dependency level and UID.
|
|
58
|
+
* For delete operations: process parents before children (descending order)
|
|
59
|
+
* For add/edit operations: process children before parents (ascending order)
|
|
60
|
+
*
|
|
61
|
+
* @param {Array} entries - Array of sync entries
|
|
62
|
+
* @param {string} operationType - Operation type: 'delete', 'add', or 'edit'
|
|
63
|
+
* @returns {Array} - Sorted entries
|
|
64
|
+
*/
|
|
65
|
+
function sortByDependency(entries, operationType) {
|
|
66
|
+
if (!Array.isArray(entries) || entries.length === 0) {
|
|
67
|
+
return [];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Group by entity type
|
|
71
|
+
const grouped = groupByEntity(entries);
|
|
72
|
+
|
|
73
|
+
// Sort entity types by dependency level
|
|
74
|
+
const entityTypes = Object.keys(grouped);
|
|
75
|
+
entityTypes.sort((a, b) => {
|
|
76
|
+
const levelA = getDependencyLevel(a);
|
|
77
|
+
const levelB = getDependencyLevel(b);
|
|
78
|
+
|
|
79
|
+
// Delete operations: parents before children (descending)
|
|
80
|
+
// Add/Edit operations: children before parents (ascending)
|
|
81
|
+
if (operationType === 'delete') {
|
|
82
|
+
return levelB - levelA; // Higher level first
|
|
83
|
+
} else {
|
|
84
|
+
return levelA - levelB; // Lower level first
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// Flatten back to array, maintaining UID sort within each entity type
|
|
89
|
+
const sorted = [];
|
|
90
|
+
for (const entity of entityTypes) {
|
|
91
|
+
const entityEntries = grouped[entity];
|
|
92
|
+
// Sort by UID for consistent ordering
|
|
93
|
+
entityEntries.sort((a, b) => (a.UID || '').localeCompare(b.UID || ''));
|
|
94
|
+
sorted.push(...entityEntries);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return sorted;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Group entries by entity type.
|
|
102
|
+
*
|
|
103
|
+
* @param {Array} entries - Array of sync entries
|
|
104
|
+
* @returns {Object} - Entries grouped by entity: { entityType: [...entries] }
|
|
105
|
+
*/
|
|
106
|
+
function groupByEntity(entries) {
|
|
107
|
+
const groups = {};
|
|
108
|
+
|
|
109
|
+
for (const entry of entries) {
|
|
110
|
+
const entity = entry.entity || entry._entity || 'unknown';
|
|
111
|
+
if (!groups[entity]) {
|
|
112
|
+
groups[entity] = [];
|
|
113
|
+
}
|
|
114
|
+
groups[entity].push(entry);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return groups;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Sort entries by UID for consistent ordering within an entity type.
|
|
122
|
+
*
|
|
123
|
+
* @param {Array} entries - Array of entries
|
|
124
|
+
* @returns {Array} - Sorted entries
|
|
125
|
+
*/
|
|
126
|
+
export function sortEntriesByUid(entries) {
|
|
127
|
+
if (!Array.isArray(entries)) {
|
|
128
|
+
return [];
|
|
129
|
+
}
|
|
130
|
+
return entries.slice().sort((a, b) => (a.UID || '').localeCompare(b.UID || ''));
|
|
131
|
+
}
|