@dboio/cli 0.19.7 → 0.20.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 +18 -12
- package/bin/dbo.js +5 -0
- package/package.json +1 -1
- package/plugins/claude/dbo/.claude-plugin/plugin.json +1 -1
- package/plugins/claude/dbo/commands/dbo.md +39 -12
- package/plugins/claude/dbo/docs/dbo-cli-readme.md +18 -12
- package/plugins/claude/dbo/skills/cookbook/SKILL.md +13 -3
- package/plugins/claude/dbo/skills/white-paper/SKILL.md +49 -8
- package/plugins/claude/dbo/skills/white-paper/references/api-reference.md +1 -1
- package/plugins/claude/track/.claude-plugin/plugin.json +1 -1
- package/src/commands/adopt.js +4 -3
- package/src/commands/clone.js +245 -55
- package/src/commands/init.js +2 -2
- package/src/commands/input.js +2 -2
- package/src/commands/login.js +3 -3
- package/src/commands/push.js +78 -18
- package/src/commands/status.js +15 -7
- package/src/lib/config.js +91 -11
- package/src/lib/filenames.js +54 -66
- package/src/lib/ignore.js +3 -0
- package/src/lib/insert.js +29 -45
- package/src/lib/structure.js +23 -8
- package/src/lib/ticketing.js +9 -8
- package/src/migrations/008-metadata-uid-in-suffix.js +4 -2
- package/src/migrations/009-fix-media-collision-metadata-names.js +9 -3
- package/src/migrations/013-remove-uid-from-meta-filenames.js +117 -0
- package/src/migrations/014-entity-dir-to-data-source.js +68 -0
package/src/lib/insert.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { readFile, readdir, writeFile, mkdir
|
|
1
|
+
import { readFile, readdir, writeFile, mkdir } from 'fs/promises';
|
|
2
2
|
import { join, dirname, basename, extname, relative } from 'path';
|
|
3
3
|
import { DboClient } from './client.js';
|
|
4
4
|
import { buildInputBody, checkSubmitErrors } from './input-parser.js';
|
|
@@ -7,7 +7,7 @@ import { log } from './logger.js';
|
|
|
7
7
|
import { shouldSkipColumn } from './columns.js';
|
|
8
8
|
import { loadAppConfig, loadAppJsonBaseline, saveAppJsonBaseline, loadExtensionDocumentationMDPlacement, loadDescriptorFilenamePreference, loadConfig } from './config.js';
|
|
9
9
|
import { loadMetadataSchema, saveMetadataSchema } from './metadata-schema.js';
|
|
10
|
-
import {
|
|
10
|
+
import { isMetadataFile } from './filenames.js';
|
|
11
11
|
import { setFileTimestamps } from './timestamps.js';
|
|
12
12
|
import { checkStoredTicket, clearGlobalTicket } from './ticketing.js';
|
|
13
13
|
import { checkModifyKey, isModifyKeyError, handleModifyKeyError } from './modify-key.js';
|
|
@@ -366,51 +366,35 @@ export async function submitAdd(meta, metaPath, filePath, client, options) {
|
|
|
366
366
|
}
|
|
367
367
|
|
|
368
368
|
if (returnedUID) {
|
|
369
|
-
//
|
|
370
|
-
const
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
log.dim(` Renamed metadata: ${basename(metaPath)} → ${basename(newMetaPath)}`);
|
|
388
|
-
log.success(`UID assigned: ${returnedUID}`);
|
|
389
|
-
|
|
390
|
-
// Restore timestamps for metadata and companion files
|
|
391
|
-
const config = await loadConfig();
|
|
392
|
-
const serverTz = config.ServerTimezone;
|
|
393
|
-
if (serverTz && returnedLastUpdated) {
|
|
394
|
-
try {
|
|
395
|
-
await setFileTimestamps(newMetaPath, returnedLastUpdated, returnedLastUpdated, serverTz);
|
|
396
|
-
for (const col of (meta._companionReferenceColumns || meta._contentColumns || [])) {
|
|
397
|
-
const ref = meta[col];
|
|
398
|
-
if (ref && String(ref).startsWith('@')) {
|
|
399
|
-
const fp = join(metaDir, String(ref).substring(1));
|
|
400
|
-
await upsertDeployEntry(fp, returnedUID, entity, col);
|
|
401
|
-
try { await setFileTimestamps(fp, returnedLastUpdated, returnedLastUpdated, serverTz); } catch {}
|
|
402
|
-
}
|
|
403
|
-
}
|
|
404
|
-
if (meta._mediaFile && String(meta._mediaFile).startsWith('@')) {
|
|
405
|
-
const mediaFp = join(metaDir, String(meta._mediaFile).substring(1));
|
|
406
|
-
await upsertDeployEntry(mediaFp, returnedUID, entity, 'File');
|
|
369
|
+
// UID lives in the JSON content — write it in-place, no filename rename needed
|
|
370
|
+
const metaDir = dirname(metaPath);
|
|
371
|
+
meta.UID = returnedUID;
|
|
372
|
+
await writeFile(metaPath, JSON.stringify(meta, null, 2) + '\n');
|
|
373
|
+
log.success(`UID assigned: ${returnedUID}`);
|
|
374
|
+
|
|
375
|
+
// Restore timestamps for metadata and companion files
|
|
376
|
+
const config = await loadConfig();
|
|
377
|
+
const serverTz = config.ServerTimezone;
|
|
378
|
+
if (serverTz && returnedLastUpdated) {
|
|
379
|
+
try {
|
|
380
|
+
await setFileTimestamps(metaPath, returnedLastUpdated, returnedLastUpdated, serverTz);
|
|
381
|
+
for (const col of (meta._companionReferenceColumns || meta._contentColumns || [])) {
|
|
382
|
+
const ref = meta[col];
|
|
383
|
+
if (ref && String(ref).startsWith('@')) {
|
|
384
|
+
const fp = join(metaDir, String(ref).substring(1));
|
|
385
|
+
await upsertDeployEntry(fp, returnedUID, entity, col);
|
|
386
|
+
try { await setFileTimestamps(fp, returnedLastUpdated, returnedLastUpdated, serverTz); } catch {}
|
|
407
387
|
}
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
388
|
+
}
|
|
389
|
+
if (meta._mediaFile && String(meta._mediaFile).startsWith('@')) {
|
|
390
|
+
const mediaFp = join(metaDir, String(meta._mediaFile).substring(1));
|
|
391
|
+
await upsertDeployEntry(mediaFp, returnedUID, entity, 'File');
|
|
392
|
+
}
|
|
393
|
+
} catch { /* non-critical */ }
|
|
413
394
|
}
|
|
395
|
+
|
|
396
|
+
// Update .app_baseline.json so subsequent pull/diff recognize this record
|
|
397
|
+
await updateBaselineWithNewRecord(meta, returnedUID, returnedLastUpdated);
|
|
414
398
|
log.dim(` Run "dbo pull -e ${entity} ${returnedUID}" to populate all columns`);
|
|
415
399
|
}
|
|
416
400
|
}
|
package/src/lib/structure.js
CHANGED
|
@@ -16,7 +16,7 @@ export const BINS_DIR = 'lib/bins';
|
|
|
16
16
|
export const SCAFFOLD_DIRS = [
|
|
17
17
|
'lib/bins',
|
|
18
18
|
'lib/app_version',
|
|
19
|
-
'lib/
|
|
19
|
+
'lib/data_source',
|
|
20
20
|
'lib/extension',
|
|
21
21
|
'lib/security',
|
|
22
22
|
'src',
|
|
@@ -32,7 +32,6 @@ export const SCAFFOLD_DIRS = [
|
|
|
32
32
|
*/
|
|
33
33
|
export const ON_DEMAND_ENTITY_DIRS = new Set([
|
|
34
34
|
'automation',
|
|
35
|
-
'data_source',
|
|
36
35
|
'entity_column',
|
|
37
36
|
'entity_column_value',
|
|
38
37
|
'group',
|
|
@@ -48,7 +47,6 @@ export const ON_DEMAND_ENTITY_DIRS = new Set([
|
|
|
48
47
|
*/
|
|
49
48
|
export const DEFAULT_PROJECT_DIRS = [
|
|
50
49
|
...SCAFFOLD_DIRS,
|
|
51
|
-
'lib/data_source',
|
|
52
50
|
'lib/group',
|
|
53
51
|
'lib/redirect',
|
|
54
52
|
'lib/site',
|
|
@@ -87,16 +85,26 @@ export const ENTITY_DIR_NAMES = new Set([
|
|
|
87
85
|
'site',
|
|
88
86
|
]);
|
|
89
87
|
|
|
88
|
+
/**
|
|
89
|
+
* Entity types that are co-located in another entity's directory.
|
|
90
|
+
* Maps entity name → directory name (both live under lib/<dirName>/).
|
|
91
|
+
*/
|
|
92
|
+
const ENTITY_DIR_REMAP = {
|
|
93
|
+
entity: 'data_source',
|
|
94
|
+
};
|
|
95
|
+
|
|
90
96
|
/**
|
|
91
97
|
* Resolve the local directory path for an entity-dir type.
|
|
92
|
-
*
|
|
98
|
+
* Most entities map to "lib/<entityName>"; remapped entities (e.g. "entity"
|
|
99
|
+
* → "lib/data_source") are defined in ENTITY_DIR_REMAP.
|
|
93
100
|
* Use this instead of bare entity name concatenation everywhere.
|
|
94
101
|
*
|
|
95
102
|
* @param {string} entityName - Entity key from ENTITY_DIR_NAMES (e.g. "extension")
|
|
96
103
|
* @returns {string} - Path relative to project root (e.g. "lib/extension")
|
|
97
104
|
*/
|
|
98
105
|
export function resolveEntityDirPath(entityName) {
|
|
99
|
-
|
|
106
|
+
const dirName = ENTITY_DIR_REMAP[entityName] ?? entityName;
|
|
107
|
+
return `${LIB_DIR}/${dirName}`;
|
|
100
108
|
}
|
|
101
109
|
|
|
102
110
|
// ─── Core Asset Entity Classification ─────────────────────────────────────
|
|
@@ -421,7 +429,9 @@ export async function loadDescriptorMapping() {
|
|
|
421
429
|
|
|
422
430
|
/**
|
|
423
431
|
* Resolve the sub-directory path for a single extension record.
|
|
424
|
-
* Returns "extension/<MappedName>"
|
|
432
|
+
* Returns "lib/extension/<MappedName>" for mapped descriptors,
|
|
433
|
+
* "lib/extension/<descriptor>" for unmapped descriptors with a value,
|
|
434
|
+
* or "lib/extension" for records with no descriptor.
|
|
425
435
|
*
|
|
426
436
|
* @param {Object} record - Extension record with a .Descriptor field
|
|
427
437
|
* @param {Object} mapping - descriptor key → dir name (from buildDescriptorMapping)
|
|
@@ -429,8 +439,13 @@ export async function loadDescriptorMapping() {
|
|
|
429
439
|
*/
|
|
430
440
|
export function resolveExtensionSubDir(record, mapping) {
|
|
431
441
|
const descriptor = record.Descriptor;
|
|
432
|
-
if (!descriptor
|
|
442
|
+
if (!descriptor) {
|
|
433
443
|
return EXTENSION_DESCRIPTORS_DIR;
|
|
434
444
|
}
|
|
435
|
-
|
|
445
|
+
if (mapping[descriptor]) {
|
|
446
|
+
return `${EXTENSION_DESCRIPTORS_DIR}/${mapping[descriptor]}`;
|
|
447
|
+
}
|
|
448
|
+
// Unmapped descriptor (no descriptor_definition record): use the raw descriptor
|
|
449
|
+
// value as the directory name — matches the directory created by buildDescriptorPrePass.
|
|
450
|
+
return `${EXTENSION_DESCRIPTORS_DIR}/${descriptor}`;
|
|
436
451
|
}
|
package/src/lib/ticketing.js
CHANGED
|
@@ -283,13 +283,14 @@ export async function checkStoredTicket(options, context = '') {
|
|
|
283
283
|
}
|
|
284
284
|
|
|
285
285
|
/**
|
|
286
|
-
*
|
|
287
|
-
* Checks per-record ticket first, then global ticket.
|
|
286
|
+
* Look up and return the active stored ticket for a submission, if no --ticket flag is set.
|
|
287
|
+
* Checks per-record ticket first, then global ticket. Callers pass the returned value
|
|
288
|
+
* to _OverrideTicketID in extraParams.
|
|
288
289
|
*
|
|
289
|
-
* @param {string[]} dataExprs -
|
|
290
|
-
* @param {string} entity -
|
|
291
|
-
* @param {string|number} rowId -
|
|
292
|
-
* @param {string} uid - Record UID for per-record lookup
|
|
290
|
+
* @param {string[]} dataExprs - Unused; kept for backwards compatibility
|
|
291
|
+
* @param {string} entity - Unused; kept for backwards compatibility
|
|
292
|
+
* @param {string|number} rowId - Unused; kept for backwards compatibility
|
|
293
|
+
* @param {string} uid - Record UID for per-record ticket lookup
|
|
293
294
|
* @param {Object} options - Command options
|
|
294
295
|
* @param {string|null} [sessionOverride] - One-time ticket override from pre-flight prompt
|
|
295
296
|
*/
|
|
@@ -301,8 +302,8 @@ export async function applyStoredTicketToSubmission(dataExprs, entity, rowId, ui
|
|
|
301
302
|
const ticketToUse = sessionOverride || recordTicket || globalTicket;
|
|
302
303
|
|
|
303
304
|
if (ticketToUse) {
|
|
304
|
-
|
|
305
|
-
|
|
305
|
+
// Don't add _LastUpdatedTicketID to dataExprs — callers set _OverrideTicketID in extraParams,
|
|
306
|
+
// which is sufficient and avoids server-side RowUID lookup failures on some entities (e.g. extension).
|
|
306
307
|
log.dim(` Applying ticket: ${ticketToUse}`);
|
|
307
308
|
return ticketToUse;
|
|
308
309
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { readdir, rename, access } from 'fs/promises';
|
|
2
2
|
import { join, basename, dirname } from 'path';
|
|
3
3
|
import { log } from '../lib/logger.js';
|
|
4
|
-
import { detectLegacyTildeMetadata
|
|
4
|
+
import { detectLegacyTildeMetadata } from '../lib/filenames.js';
|
|
5
5
|
|
|
6
6
|
export const description = 'Rename metadata files from name~uid.metadata.json to name.metadata~uid.json';
|
|
7
7
|
|
|
@@ -32,7 +32,9 @@ export default async function run(_options) {
|
|
|
32
32
|
if (!parsed) continue;
|
|
33
33
|
|
|
34
34
|
const { naturalBase, uid } = parsed;
|
|
35
|
-
|
|
35
|
+
// Migration 008 target is the intermediate name.metadata~uid.json format;
|
|
36
|
+
// migration 013 will later remove the uid suffix entirely.
|
|
37
|
+
const newFilename = `${naturalBase}.metadata~${uid}.json`;
|
|
36
38
|
const newPath = join(dir, newFilename);
|
|
37
39
|
|
|
38
40
|
// Skip if target already exists (avoid overwrite)
|
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
import { readdir, readFile, rename, unlink, access } from 'fs/promises';
|
|
2
2
|
import { join, basename, dirname, extname } from 'path';
|
|
3
3
|
import { log } from '../lib/logger.js';
|
|
4
|
-
import { parseMetaFilename
|
|
4
|
+
import { parseMetaFilename } from '../lib/filenames.js';
|
|
5
|
+
|
|
6
|
+
// Build the intermediate name.metadata~uid.json format this migration targets.
|
|
7
|
+
// (Migration 013 will later strip the uid from filenames entirely.)
|
|
8
|
+
function buildLegacySuffixFilename(naturalBase, uid) {
|
|
9
|
+
return `${naturalBase}.metadata~${uid}.json`;
|
|
10
|
+
}
|
|
5
11
|
|
|
6
12
|
export const description = 'Fix media collision suffix: rename (media) → _media and fix mismatched metadata filenames';
|
|
7
13
|
|
|
@@ -87,7 +93,7 @@ export default async function run(_options) {
|
|
|
87
93
|
// Rename metadata file itself if it contains (media)
|
|
88
94
|
if (parsed.naturalBase.includes('(media)')) {
|
|
89
95
|
const newBase = parsed.naturalBase.replace('(media)', '_media');
|
|
90
|
-
const newMetaFilename =
|
|
96
|
+
const newMetaFilename = buildLegacySuffixFilename(newBase, parsed.uid);
|
|
91
97
|
const newMetaPath = join(dirname(metaPath), newMetaFilename);
|
|
92
98
|
try { await access(newMetaPath); } catch {
|
|
93
99
|
await rename(metaPath, newMetaPath);
|
|
@@ -112,7 +118,7 @@ export default async function run(_options) {
|
|
|
112
118
|
// Already correct
|
|
113
119
|
if (currentParsed.naturalBase === refFilename) continue;
|
|
114
120
|
|
|
115
|
-
const correctFilename =
|
|
121
|
+
const correctFilename = buildLegacySuffixFilename(refFilename, currentParsed.uid);
|
|
116
122
|
const correctPath = join(dirname(metaPath), correctFilename);
|
|
117
123
|
|
|
118
124
|
// If correct metadata already exists, this one is an orphan
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { readFile, writeFile, rename, readdir, access } from 'fs/promises';
|
|
2
|
+
import { join, basename, dirname } from 'path';
|
|
3
|
+
|
|
4
|
+
export const description = 'Rename metadata files from name.metadata~uid.json to name.metadata.json';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Migration 013 — Remove UID from metadata filenames.
|
|
8
|
+
*
|
|
9
|
+
* Old format: colors.metadata~abc123.json
|
|
10
|
+
* New format: colors.metadata.json
|
|
11
|
+
*
|
|
12
|
+
* The UID is already stored inside the JSON as the "UID" field — no information is lost.
|
|
13
|
+
*
|
|
14
|
+
* Collision resolution uses entity priority:
|
|
15
|
+
* content > output > everything else
|
|
16
|
+
*
|
|
17
|
+
* Within the same entity type, the first record (alphabetically by old filename) wins
|
|
18
|
+
* the unsuffixed slot; subsequent ones get -1, -2, etc.
|
|
19
|
+
*
|
|
20
|
+
* Does NOT rename companion files (they use natural names already).
|
|
21
|
+
*/
|
|
22
|
+
export default async function run(_options) {
|
|
23
|
+
const cwd = process.cwd();
|
|
24
|
+
let totalRenamed = 0;
|
|
25
|
+
|
|
26
|
+
const legacyFiles = await findLegacySuffixMetadataFiles(cwd);
|
|
27
|
+
if (legacyFiles.length === 0) return;
|
|
28
|
+
|
|
29
|
+
// Group by directory so we can resolve collisions per-dir
|
|
30
|
+
const byDir = new Map();
|
|
31
|
+
for (const filePath of legacyFiles) {
|
|
32
|
+
const dir = dirname(filePath);
|
|
33
|
+
if (!byDir.has(dir)) byDir.set(dir, []);
|
|
34
|
+
byDir.get(dir).push(filePath);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
for (const [dir, files] of byDir) {
|
|
38
|
+
// Read entity type from each file to apply priority ordering
|
|
39
|
+
const withMeta = [];
|
|
40
|
+
for (const filePath of files) {
|
|
41
|
+
let entity = 'other';
|
|
42
|
+
let uid = null;
|
|
43
|
+
try {
|
|
44
|
+
const content = JSON.parse(await readFile(filePath, 'utf8'));
|
|
45
|
+
entity = content._entity || 'other';
|
|
46
|
+
uid = content.UID || null;
|
|
47
|
+
} catch { /* use defaults */ }
|
|
48
|
+
|
|
49
|
+
// Parse naturalBase from legacy filename: name.metadata~uid.json
|
|
50
|
+
const filename = basename(filePath);
|
|
51
|
+
const m = filename.match(/^(.+)\.metadata~([a-z0-9_]+)\.json$/i);
|
|
52
|
+
const naturalBase = m ? m[1] : filename.replace(/\.metadata~[^.]+\.json$/i, '');
|
|
53
|
+
|
|
54
|
+
withMeta.push({ filePath, naturalBase, entity, uid });
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Sort by entity priority: content first, output second, then others (alphabetically within tier)
|
|
58
|
+
const PRIORITY = { content: 0, output: 1 };
|
|
59
|
+
withMeta.sort((a, b) => {
|
|
60
|
+
const pa = PRIORITY[a.entity] ?? 2;
|
|
61
|
+
const pb = PRIORITY[b.entity] ?? 2;
|
|
62
|
+
if (pa !== pb) return pa - pb;
|
|
63
|
+
return a.naturalBase.localeCompare(b.naturalBase);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// Assign new filenames with collision resolution
|
|
67
|
+
const usedBases = new Map(); // naturalBase (lowercase) → count of times used
|
|
68
|
+
|
|
69
|
+
for (const { filePath, naturalBase, entity } of withMeta) {
|
|
70
|
+
const baseKey = naturalBase.toLowerCase();
|
|
71
|
+
const count = usedBases.get(baseKey) || 0;
|
|
72
|
+
usedBases.set(baseKey, count + 1);
|
|
73
|
+
|
|
74
|
+
const newFilename = count === 0
|
|
75
|
+
? `${naturalBase}.metadata.json`
|
|
76
|
+
: `${naturalBase}-${count}.metadata.json`;
|
|
77
|
+
|
|
78
|
+
const newPath = join(dir, newFilename);
|
|
79
|
+
|
|
80
|
+
// Skip if target already exists (safe guard against re-running migration)
|
|
81
|
+
try { await access(newPath); continue; } catch { /* doesn't exist — safe to rename */ }
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
await rename(filePath, newPath);
|
|
85
|
+
if (newFilename !== basename(filePath)) {
|
|
86
|
+
console.log(` [${entity}] ${basename(filePath)} → ${newFilename}`);
|
|
87
|
+
totalRenamed++;
|
|
88
|
+
}
|
|
89
|
+
} catch (err) {
|
|
90
|
+
console.warn(` (skip) Could not rename ${basename(filePath)}: ${err.message}`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (totalRenamed > 0) {
|
|
96
|
+
console.log(` Renamed ${totalRenamed} metadata file(s) — UID now stored in JSON only`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const SKIP = new Set(['.app', 'node_modules', 'trash', '.git', '.claude', 'app_dependencies']);
|
|
101
|
+
|
|
102
|
+
async function findLegacySuffixMetadataFiles(dir) {
|
|
103
|
+
const results = [];
|
|
104
|
+
try {
|
|
105
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
106
|
+
for (const entry of entries) {
|
|
107
|
+
if (SKIP.has(entry.name)) continue;
|
|
108
|
+
const full = join(dir, entry.name);
|
|
109
|
+
if (entry.isDirectory()) {
|
|
110
|
+
results.push(...await findLegacySuffixMetadataFiles(full));
|
|
111
|
+
} else if (/\.metadata~[a-z0-9_]+\.json$/i.test(entry.name)) {
|
|
112
|
+
results.push(full);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
} catch { /* skip unreadable dirs */ }
|
|
116
|
+
return results;
|
|
117
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { readdir, rename, mkdir, rmdir, access } from 'fs/promises';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { log } from '../lib/logger.js';
|
|
4
|
+
|
|
5
|
+
export const description = 'Move lib/entity/ files into lib/data_source/';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Migration 014 — Relocate entity records from lib/entity/ into lib/data_source/.
|
|
9
|
+
*
|
|
10
|
+
* Entity (table-definition) records are now co-located with data source records
|
|
11
|
+
* under lib/data_source/. Files keep their _entity: "entity" metadata value —
|
|
12
|
+
* only their directory changes.
|
|
13
|
+
*
|
|
14
|
+
* If lib/data_source/ already contains a file with the same name, the source
|
|
15
|
+
* file is left in place and a warning is emitted; no data is overwritten.
|
|
16
|
+
*/
|
|
17
|
+
export default async function run() {
|
|
18
|
+
const cwd = process.cwd();
|
|
19
|
+
const srcDir = join(cwd, 'lib', 'entity');
|
|
20
|
+
|
|
21
|
+
// Nothing to do if lib/entity/ doesn't exist
|
|
22
|
+
try {
|
|
23
|
+
await access(srcDir);
|
|
24
|
+
} catch {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const entries = await readdir(srcDir, { withFileTypes: true });
|
|
29
|
+
if (entries.length === 0) {
|
|
30
|
+
// Empty directory — just remove it
|
|
31
|
+
try { await rmdir(srcDir); } catch { /* ignore */ }
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const destDir = join(cwd, 'lib', 'data_source');
|
|
36
|
+
await mkdir(destDir, { recursive: true });
|
|
37
|
+
|
|
38
|
+
let movedCount = 0;
|
|
39
|
+
let skippedCount = 0;
|
|
40
|
+
|
|
41
|
+
for (const entry of entries) {
|
|
42
|
+
const srcPath = join(srcDir, entry.name);
|
|
43
|
+
const destPath = join(destDir, entry.name);
|
|
44
|
+
|
|
45
|
+
// Check for collision
|
|
46
|
+
try {
|
|
47
|
+
await access(destPath);
|
|
48
|
+
log.warn(` Migration 014: skipped ${entry.name} — already exists in lib/data_source/`);
|
|
49
|
+
skippedCount++;
|
|
50
|
+
continue;
|
|
51
|
+
} catch { /* dest absent — safe to move */ }
|
|
52
|
+
|
|
53
|
+
await rename(srcPath, destPath);
|
|
54
|
+
movedCount++;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Remove lib/entity/ if now empty
|
|
58
|
+
try {
|
|
59
|
+
const remaining = await readdir(srcDir);
|
|
60
|
+
if (remaining.length === 0) {
|
|
61
|
+
await rmdir(srcDir);
|
|
62
|
+
}
|
|
63
|
+
} catch { /* ignore */ }
|
|
64
|
+
|
|
65
|
+
if (movedCount > 0) {
|
|
66
|
+
log.success(` Migration 014: moved ${movedCount} file(s) from lib/entity/ → lib/data_source/${skippedCount > 0 ? ` (${skippedCount} skipped — collision)` : ''}`);
|
|
67
|
+
}
|
|
68
|
+
}
|