@dboio/cli 0.13.2 → 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 +57 -0
- package/bin/dbo.js +2 -0
- package/package.json +1 -1
- package/plugins/claude/dbo/.claude-plugin/plugin.json +1 -1
- package/plugins/claude/dbo/commands/dbo.md +76 -74
- package/plugins/claude/dbo/docs/dbo-cli-readme.md +57 -0
- package/plugins/claude/dbo/skills/cli/SKILL.md +2 -1
- package/src/commands/add.js +12 -7
- package/src/commands/clone.js +138 -94
- package/src/commands/deploy.js +9 -2
- package/src/commands/diff.js +4 -4
- package/src/commands/login.js +30 -4
- package/src/commands/mv.js +17 -4
- package/src/commands/push.js +34 -9
- package/src/commands/rm.js +6 -4
- package/src/commands/tag.js +65 -0
- package/src/lib/config.js +28 -0
- package/src/lib/deploy-config.js +137 -0
- package/src/lib/diff.js +5 -4
- package/src/lib/filenames.js +89 -24
- package/src/lib/scaffold.js +1 -1
- package/src/lib/tagging.js +380 -0
- package/src/lib/toe-stepping.js +2 -1
- package/src/migrations/006-remove-uid-companion-filenames.js +3 -3
- 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,165 @@
|
|
|
1
|
+
import { readdir, readFile, writeFile, rename, access, mkdir } from 'fs/promises';
|
|
2
|
+
import { join, basename, dirname } from 'path';
|
|
3
|
+
import { log } from '../lib/logger.js';
|
|
4
|
+
import { stripUidFromFilename, hasUidInFilename, isMetadataFile } from '../lib/filenames.js';
|
|
5
|
+
import { ensureTrashIcon } from '../lib/tagging.js';
|
|
6
|
+
|
|
7
|
+
export const description = 'Rename entity/extension companion files from name~uid.Col.ext to name.Col.ext';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Migration 007 — Strip ~UID from entity and extension companion files.
|
|
11
|
+
*
|
|
12
|
+
* Previous convention: entity companion files included ~UID in the filename,
|
|
13
|
+
* e.g. "Add-Asst-Execute-Security~wxl6ivcwfkix3zgantszjg.String16.js".
|
|
14
|
+
*
|
|
15
|
+
* New convention: only .metadata.json files carry ~UID. Companion files use
|
|
16
|
+
* the natural base name, e.g. "Add-Asst-Execute-Security.String16.js".
|
|
17
|
+
*
|
|
18
|
+
* For each metadata file in entity directories (lib/extension/, lib/automation/,
|
|
19
|
+
* lib/data_source/, etc.):
|
|
20
|
+
* 1. Check each @reference in _contentColumns
|
|
21
|
+
* 2. If the reference contains ~UID, strip it, rename the file, update the reference
|
|
22
|
+
* 3. If both natural and legacy files exist, move the legacy to trash/
|
|
23
|
+
*
|
|
24
|
+
* Also handles name collisions: if stripping ~UID from two different records
|
|
25
|
+
* produces the same natural companion name, the second keeps its ~UID reference
|
|
26
|
+
* to avoid data loss.
|
|
27
|
+
*/
|
|
28
|
+
export default async function run(_options) {
|
|
29
|
+
const cwd = process.cwd();
|
|
30
|
+
let totalRenamed = 0;
|
|
31
|
+
let totalRefsUpdated = 0;
|
|
32
|
+
|
|
33
|
+
const metaFiles = await findEntityMetadataFiles(cwd);
|
|
34
|
+
if (metaFiles.length === 0) return;
|
|
35
|
+
|
|
36
|
+
// Track natural names we've claimed, to detect collisions
|
|
37
|
+
// Key: absolute natural path → true
|
|
38
|
+
const claimedNaturals = new Set();
|
|
39
|
+
|
|
40
|
+
for (const metaPath of metaFiles) {
|
|
41
|
+
try {
|
|
42
|
+
const meta = JSON.parse(await readFile(metaPath, 'utf8'));
|
|
43
|
+
const uid = meta.UID;
|
|
44
|
+
if (!uid) continue;
|
|
45
|
+
|
|
46
|
+
const metaDir = dirname(metaPath);
|
|
47
|
+
const contentCols = [...(meta._contentColumns || [])];
|
|
48
|
+
if (meta._mediaFile) contentCols.push('_mediaFile');
|
|
49
|
+
let metaChanged = false;
|
|
50
|
+
|
|
51
|
+
for (const col of contentCols) {
|
|
52
|
+
const ref = meta[col];
|
|
53
|
+
if (!ref || !String(ref).startsWith('@')) continue;
|
|
54
|
+
|
|
55
|
+
const refName = String(ref).substring(1);
|
|
56
|
+
|
|
57
|
+
// Only process references that contain ~UID
|
|
58
|
+
if (!hasUidInFilename(refName, uid)) continue;
|
|
59
|
+
|
|
60
|
+
const naturalName = stripUidFromFilename(refName, uid);
|
|
61
|
+
const legacyPath = refName.startsWith('/')
|
|
62
|
+
? join(cwd, refName)
|
|
63
|
+
: join(metaDir, refName);
|
|
64
|
+
const naturalPath = refName.startsWith('/')
|
|
65
|
+
? join(cwd, naturalName)
|
|
66
|
+
: join(metaDir, naturalName);
|
|
67
|
+
|
|
68
|
+
// Collision check: if another record already claimed this natural path, skip
|
|
69
|
+
if (claimedNaturals.has(naturalPath)) {
|
|
70
|
+
log.dim(` Skipped ${refName} (natural name collision with another record)`);
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
let legacyExists = false;
|
|
75
|
+
let naturalExists = false;
|
|
76
|
+
try { await access(legacyPath); legacyExists = true; } catch { /* missing */ }
|
|
77
|
+
try { await access(naturalPath); naturalExists = true; } catch { /* missing */ }
|
|
78
|
+
|
|
79
|
+
if (legacyExists && !naturalExists) {
|
|
80
|
+
// Rename legacy → natural
|
|
81
|
+
try {
|
|
82
|
+
await rename(legacyPath, naturalPath);
|
|
83
|
+
log.dim(` ${refName} → ${naturalName}`);
|
|
84
|
+
totalRenamed++;
|
|
85
|
+
} catch { /* rename failed — keep reference as-is */ continue; }
|
|
86
|
+
} else if (legacyExists && naturalExists) {
|
|
87
|
+
// Both exist — move orphaned legacy to trash
|
|
88
|
+
try {
|
|
89
|
+
const trashDir = join(cwd, 'trash');
|
|
90
|
+
await mkdir(trashDir, { recursive: true });
|
|
91
|
+
await rename(legacyPath, join(trashDir, basename(legacyPath)));
|
|
92
|
+
await ensureTrashIcon(trashDir);
|
|
93
|
+
log.dim(` Trashed orphan: ${refName}`);
|
|
94
|
+
totalRenamed++;
|
|
95
|
+
} catch { /* non-critical */ }
|
|
96
|
+
}
|
|
97
|
+
// else: legacy doesn't exist — just update the reference
|
|
98
|
+
|
|
99
|
+
// Update @reference to natural name
|
|
100
|
+
meta[col] = `@${naturalName}`;
|
|
101
|
+
metaChanged = true;
|
|
102
|
+
claimedNaturals.add(naturalPath);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Rewrite metadata if @references were updated
|
|
106
|
+
if (metaChanged) {
|
|
107
|
+
try {
|
|
108
|
+
await writeFile(metaPath, JSON.stringify(meta, null, 2) + '\n');
|
|
109
|
+
totalRefsUpdated++;
|
|
110
|
+
} catch { /* non-critical */ }
|
|
111
|
+
}
|
|
112
|
+
} catch { /* skip unreadable metadata */ }
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (totalRenamed > 0 || totalRefsUpdated > 0) {
|
|
116
|
+
log.dim(` Renamed ${totalRenamed} entity companion file(s), updated ${totalRefsUpdated} metadata @reference(s)`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Directories that hold entity/extension metadata (under lib/) */
|
|
121
|
+
const ENTITY_DIRS = new Set([
|
|
122
|
+
'extension', 'automation', 'app_version', 'entity', 'entity_column',
|
|
123
|
+
'entity_column_value', 'integration', 'security', 'security_column',
|
|
124
|
+
'data_source', 'group', 'site', 'redirect',
|
|
125
|
+
]);
|
|
126
|
+
|
|
127
|
+
const SKIP = new Set(['.dbo', 'node_modules', 'trash', '.git', '.claude']);
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Find all .metadata.json files inside entity directories (lib/<entity>/).
|
|
131
|
+
* Skips content/media in Bins/ — those are handled by migration 006.
|
|
132
|
+
*/
|
|
133
|
+
async function findEntityMetadataFiles(cwd) {
|
|
134
|
+
const results = [];
|
|
135
|
+
const libDir = join(cwd, 'lib');
|
|
136
|
+
|
|
137
|
+
let libEntries;
|
|
138
|
+
try { libEntries = await readdir(libDir, { withFileTypes: true }); } catch { return results; }
|
|
139
|
+
|
|
140
|
+
for (const entry of libEntries) {
|
|
141
|
+
if (!entry.isDirectory()) continue;
|
|
142
|
+
if (SKIP.has(entry.name)) continue;
|
|
143
|
+
if (!ENTITY_DIRS.has(entry.name)) continue;
|
|
144
|
+
|
|
145
|
+
await scanDir(join(libDir, entry.name), results);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return results;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async function scanDir(dir, results) {
|
|
152
|
+
let entries;
|
|
153
|
+
try { entries = await readdir(dir, { withFileTypes: true }); } catch { return; }
|
|
154
|
+
|
|
155
|
+
for (const entry of entries) {
|
|
156
|
+
if (SKIP.has(entry.name) || entry.name.startsWith('.')) continue;
|
|
157
|
+
const full = join(dir, entry.name);
|
|
158
|
+
|
|
159
|
+
if (entry.isDirectory()) {
|
|
160
|
+
await scanDir(full, results);
|
|
161
|
+
} else if (isMetadataFile(entry.name)) {
|
|
162
|
+
results.push(full);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { readdir, rename, access } from 'fs/promises';
|
|
2
|
+
import { join, basename, dirname } from 'path';
|
|
3
|
+
import { log } from '../lib/logger.js';
|
|
4
|
+
import { detectLegacyTildeMetadata, buildMetaFilename } from '../lib/filenames.js';
|
|
5
|
+
|
|
6
|
+
export const description = 'Rename metadata files from name~uid.metadata.json to name.metadata~uid.json';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Migration 008 — Move UID from metadata filename base to suffix.
|
|
10
|
+
*
|
|
11
|
+
* Old format: colors~abc123.metadata.json
|
|
12
|
+
* New format: colors.metadata~abc123.json
|
|
13
|
+
*
|
|
14
|
+
* Also handles media metadata: logo~uid.png.metadata.json → logo.png.metadata~uid.json
|
|
15
|
+
*
|
|
16
|
+
* Does NOT rename companion files (they are already natural-named).
|
|
17
|
+
* Updates no @references — @references point to companion files, which are unchanged.
|
|
18
|
+
*/
|
|
19
|
+
export default async function run(_options) {
|
|
20
|
+
const cwd = process.cwd();
|
|
21
|
+
let totalRenamed = 0;
|
|
22
|
+
|
|
23
|
+
const metaFiles = await findAllLegacyMetadataFiles(cwd);
|
|
24
|
+
if (metaFiles.length === 0) return;
|
|
25
|
+
|
|
26
|
+
for (const metaPath of metaFiles) {
|
|
27
|
+
const filename = basename(metaPath);
|
|
28
|
+
const dir = dirname(metaPath);
|
|
29
|
+
|
|
30
|
+
// Detect legacy tilde format: name~uid.metadata.json or name~uid.ext.metadata.json
|
|
31
|
+
const parsed = detectLegacyTildeMetadata(filename);
|
|
32
|
+
if (!parsed) continue;
|
|
33
|
+
|
|
34
|
+
const { naturalBase, uid } = parsed;
|
|
35
|
+
const newFilename = buildMetaFilename(naturalBase, uid);
|
|
36
|
+
const newPath = join(dir, newFilename);
|
|
37
|
+
|
|
38
|
+
// Skip if target already exists (avoid overwrite)
|
|
39
|
+
try { await access(newPath); continue; } catch { /* doesn't exist, safe to rename */ }
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
await rename(metaPath, newPath);
|
|
43
|
+
log.dim(` ${filename} → ${newFilename}`);
|
|
44
|
+
totalRenamed++;
|
|
45
|
+
} catch { /* non-critical: leave as-is */ }
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (totalRenamed > 0) {
|
|
49
|
+
log.dim(` Renamed ${totalRenamed} metadata file(s) to new suffix convention`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const SKIP = new Set(['.dbo', 'node_modules', 'trash', '.git', '.claude']);
|
|
54
|
+
|
|
55
|
+
async function findAllLegacyMetadataFiles(dir) {
|
|
56
|
+
const results = [];
|
|
57
|
+
try {
|
|
58
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
59
|
+
for (const entry of entries) {
|
|
60
|
+
if (SKIP.has(entry.name)) continue;
|
|
61
|
+
const full = join(dir, entry.name);
|
|
62
|
+
if (entry.isDirectory()) {
|
|
63
|
+
results.push(...await findAllLegacyMetadataFiles(full));
|
|
64
|
+
} else if (detectLegacyTildeMetadata(entry.name)) {
|
|
65
|
+
results.push(full);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
} catch { /* skip unreadable dirs */ }
|
|
69
|
+
return results;
|
|
70
|
+
}
|