@dboio/cli 0.10.1 → 0.11.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.
@@ -0,0 +1,190 @@
1
+ import { readFile, writeFile, mkdir, rename, access, readdir, rmdir } from 'fs/promises';
2
+ import { join } from 'path';
3
+ import { log } from '../lib/logger.js';
4
+
5
+ export const description = 'Relocated server entity directories into lib/';
6
+
7
+ /** Entity dirs that should live under lib/ (the full list, in detection order) */
8
+ const ENTITY_DIRS_TO_MOVE = [
9
+ 'bins', 'extension', 'automation', 'app_version', 'entity',
10
+ 'entity_column', 'entity_column_value', 'integration', 'security',
11
+ 'security_column', 'data_source', 'group', 'site', 'redirect',
12
+ ];
13
+
14
+ /**
15
+ * Migration 002 — Move root-level entity directories into lib/.
16
+ *
17
+ * Detects old flat-root layout, moves dirs to lib/, rewrites app.json @path
18
+ * references, updates structure.json extensionDescriptorDirs, renames tests/ → test/.
19
+ *
20
+ * @param {object} _options - Command options (unused)
21
+ */
22
+ export default async function run(_options) {
23
+ const cwd = process.cwd();
24
+
25
+ // Detect which old-layout dirs exist at root
26
+ const detected = [];
27
+ for (const dirName of ENTITY_DIRS_TO_MOVE) {
28
+ try {
29
+ await access(join(cwd, dirName));
30
+ detected.push(dirName);
31
+ } catch { /* not present */ }
32
+ }
33
+
34
+ if (detected.length === 0) {
35
+ // Check for tests/ → test/ rename even if no entity dirs to move
36
+ await renameTestsDir(cwd);
37
+ return;
38
+ }
39
+
40
+ // Create lib/ directory
41
+ await mkdir(join(cwd, 'lib'), { recursive: true });
42
+
43
+ let movedCount = 0;
44
+ for (const dirName of detected) {
45
+ const src = join(cwd, dirName);
46
+ const dest = join(cwd, 'lib', dirName);
47
+
48
+ try {
49
+ await access(dest);
50
+ // Destination already exists — merge: move files from src not in dest
51
+ await mergeDir(src, dest);
52
+ log.dim(` ✓ Merged ${dirName}/ → lib/${dirName}/ (partial migration detected)`);
53
+ } catch {
54
+ // Destination absent — simple rename
55
+ await rename(src, dest);
56
+ log.dim(` ✓ Moved ${dirName}/ → lib/${dirName}/`);
57
+ }
58
+ movedCount++;
59
+ }
60
+
61
+ // Rewrite app.json @path references
62
+ await rewriteAppJson(cwd, detected);
63
+
64
+ // Update .dbo/structure.json extensionDescriptorDirs
65
+ await rewriteStructureJson(cwd);
66
+
67
+ // Rename tests/ → test/ if present
68
+ await renameTestsDir(cwd);
69
+
70
+ log.success(` Migration 002: Relocated ${movedCount} server director${movedCount === 1 ? 'y' : 'ies'} into lib/`);
71
+ }
72
+
73
+ /**
74
+ * Merge contents of srcDir into destDir without overwriting existing files.
75
+ * Recursively processes subdirectories.
76
+ */
77
+ async function mergeDir(srcDir, destDir) {
78
+ const entries = await readdir(srcDir, { withFileTypes: true });
79
+ await mkdir(destDir, { recursive: true });
80
+
81
+ for (const entry of entries) {
82
+ const srcPath = join(srcDir, entry.name);
83
+ const destPath = join(destDir, entry.name);
84
+
85
+ if (entry.isDirectory()) {
86
+ await mergeDir(srcPath, destPath);
87
+ } else {
88
+ try {
89
+ await access(destPath);
90
+ // File already in dest — skip to avoid overwriting
91
+ } catch {
92
+ await rename(srcPath, destPath);
93
+ }
94
+ }
95
+ }
96
+
97
+ // Remove now-empty source dir (best-effort)
98
+ try {
99
+ const remaining = await readdir(srcDir);
100
+ if (remaining.length === 0) {
101
+ await rmdir(srcDir);
102
+ }
103
+ } catch { /* ignore */ }
104
+ }
105
+
106
+ /**
107
+ * Rewrite app.json @path references from old root-level paths to lib/-prefixed paths.
108
+ * e.g. "@extension/foo.metadata.json" → "@lib/extension/foo.metadata.json"
109
+ * "@bins/app/styles.css" → "@lib/bins/app/styles.css"
110
+ */
111
+ async function rewriteAppJson(cwd, movedDirs) {
112
+ const appJsonPath = join(cwd, 'app.json');
113
+ let raw;
114
+ try {
115
+ raw = await readFile(appJsonPath, 'utf8');
116
+ } catch {
117
+ return; // no app.json — skip
118
+ }
119
+
120
+ let updated = raw;
121
+ for (const dirName of movedDirs) {
122
+ // Replace "@dirName/" with "@lib/dirName/"
123
+ // Use a global regex that handles the @ prefix in JSON string values
124
+ const pattern = new RegExp(`"@${dirName}/`, 'g');
125
+ updated = updated.replace(pattern, `"@lib/${dirName}/`);
126
+ }
127
+
128
+ if (updated !== raw) {
129
+ await writeFile(appJsonPath, updated);
130
+ log.dim(' ✓ Rewrote app.json @path references to lib/ prefix');
131
+ }
132
+ }
133
+
134
+ /**
135
+ * Update .dbo/structure.json extensionDescriptorDirs from "extension/..." to "lib/extension/...".
136
+ */
137
+ async function rewriteStructureJson(cwd) {
138
+ const structurePath = join(cwd, '.dbo', 'structure.json');
139
+ let raw;
140
+ try {
141
+ raw = await readFile(structurePath, 'utf8');
142
+ } catch {
143
+ return; // no structure.json — skip
144
+ }
145
+
146
+ let structure;
147
+ try {
148
+ structure = JSON.parse(raw);
149
+ } catch {
150
+ return;
151
+ }
152
+
153
+ if (!Array.isArray(structure.extensionDescriptorDirs)) return;
154
+
155
+ const updated = structure.extensionDescriptorDirs.map(dir => {
156
+ if (dir.startsWith('extension/') || dir === 'extension/_unsupported') {
157
+ return `lib/${dir}`;
158
+ }
159
+ // Already prefixed (re-entrant safety)
160
+ return dir;
161
+ });
162
+
163
+ structure.extensionDescriptorDirs = updated;
164
+ await writeFile(structurePath, JSON.stringify(structure, null, 2) + '\n');
165
+ log.dim(' ✓ Updated .dbo/structure.json extensionDescriptorDirs to lib/ prefix');
166
+ }
167
+
168
+ /**
169
+ * Rename tests/ → test/ at project root if tests/ exists and test/ does not.
170
+ */
171
+ async function renameTestsDir(cwd) {
172
+ const testsSrc = join(cwd, 'tests');
173
+ const testDest = join(cwd, 'test');
174
+
175
+ try {
176
+ await access(testsSrc);
177
+ } catch {
178
+ return; // tests/ doesn't exist — nothing to do
179
+ }
180
+
181
+ try {
182
+ await access(testDest);
183
+ // test/ already exists — skip rename to avoid conflict
184
+ log.dim(' ⚠ Both tests/ and test/ exist — tests/ was not renamed; resolve manually');
185
+ return;
186
+ } catch { /* test/ absent — safe to rename */ }
187
+
188
+ await rename(testsSrc, testDest);
189
+ log.dim(' ✓ Renamed tests/ → test/');
190
+ }
@@ -0,0 +1,50 @@
1
+ import { access, rename, readFile, writeFile } from 'fs/promises';
2
+ import { join } from 'path';
3
+ import { log } from '../lib/logger.js';
4
+
5
+ export const description = 'Moved deploy manifest from dbo.deploy.json to .dbo/deploy_config.json';
6
+
7
+ /**
8
+ * Migration 003 — Move dbo.deploy.json → .dbo/deploy_config.json
9
+ *
10
+ * The deploy manifest now lives inside .dbo/ alongside other config files.
11
+ * Also removes the legacy `dbo.deploy.json` entry from .dboignore if present.
12
+ *
13
+ * @param {object} _options - Command options (unused)
14
+ */
15
+ export default async function run(_options) {
16
+ const cwd = process.cwd();
17
+ const legacyPath = join(cwd, 'dbo.deploy.json');
18
+ const newPath = join(cwd, '.dbo', 'deploy_config.json');
19
+
20
+ // Check if legacy file exists
21
+ try {
22
+ await access(legacyPath);
23
+ } catch {
24
+ return; // No legacy file — nothing to do
25
+ }
26
+
27
+ // Check if new location already exists (avoid overwriting)
28
+ try {
29
+ await access(newPath);
30
+ log.dim(' ⚠ Both dbo.deploy.json and .dbo/deploy_config.json exist — skipping move; resolve manually');
31
+ return;
32
+ } catch { /* new path absent — safe to move */ }
33
+
34
+ await rename(legacyPath, newPath);
35
+ log.dim(' ✓ Moved dbo.deploy.json → .dbo/deploy_config.json');
36
+
37
+ // Clean up .dboignore — remove the dbo.deploy.json entry if present
38
+ const dboignorePath = join(cwd, '.dboignore');
39
+ try {
40
+ const content = await readFile(dboignorePath, 'utf8');
41
+ const updated = content
42
+ .split('\n')
43
+ .filter(line => line.trim() !== 'dbo.deploy.json')
44
+ .join('\n');
45
+ if (updated !== content) {
46
+ await writeFile(dboignorePath, updated);
47
+ log.dim(' ✓ Removed dbo.deploy.json from .dboignore');
48
+ }
49
+ } catch { /* no .dboignore — skip */ }
50
+ }
@@ -0,0 +1,101 @@
1
+ import { readdir, readFile, writeFile, rename, access } from 'fs/promises';
2
+ import { join, dirname } from 'path';
3
+ import { log } from '../lib/logger.js';
4
+
5
+ export const description = 'Rename _output~ files to Name~UID convention';
6
+
7
+ const OUTPUT_PREFIX = '_output~';
8
+
9
+ /**
10
+ * Migration 004 — Rename output hierarchy files to drop the _output~ prefix.
11
+ *
12
+ * Old: _output~task.info~UID.json, _output~task.info~UID.column~ColUID.CustomSQL.sql
13
+ * New: task.info~UID.json, task.info~UID.column~ColUID.CustomSQL.sql
14
+ *
15
+ * Also rewrites @_output~ references inside the root JSON files.
16
+ *
17
+ * @param {object} _options - Command options (unused)
18
+ */
19
+ export default async function run(_options) {
20
+ const cwd = process.cwd();
21
+ let totalRenamed = 0;
22
+
23
+ // Recursively find directories that contain _output~ files
24
+ const dirs = await findDirsWithOutputFiles(cwd);
25
+
26
+ for (const dir of dirs) {
27
+ const entries = await readdir(dir);
28
+ const outputFiles = entries.filter(f => f.startsWith(OUTPUT_PREFIX));
29
+ if (outputFiles.length === 0) continue;
30
+
31
+ for (const oldName of outputFiles) {
32
+ const newName = oldName.substring(OUTPUT_PREFIX.length);
33
+ const oldPath = join(dir, oldName);
34
+ const newPath = join(dir, newName);
35
+
36
+ // Skip if destination already exists (avoid overwrite)
37
+ try {
38
+ await access(newPath);
39
+ continue;
40
+ } catch { /* good — doesn't exist */ }
41
+
42
+ await rename(oldPath, newPath);
43
+ totalRenamed++;
44
+
45
+ // If it's a root JSON, also rewrite @_output~ references inside it
46
+ if (newName.endsWith('.json') && !newName.includes('.CustomSQL.')) {
47
+ await rewriteReferences(newPath);
48
+ }
49
+ }
50
+ }
51
+
52
+ if (totalRenamed > 0) {
53
+ log.dim(` Renamed ${totalRenamed} output file${totalRenamed === 1 ? '' : 's'} (dropped _output~ prefix)`);
54
+ }
55
+ }
56
+
57
+ /**
58
+ * Rewrite @_output~ references in a JSON file's string values.
59
+ * e.g. "@_output~task.info~UID.column~ColUID.CustomSQL.sql"
60
+ * → "@task.info~UID.column~ColUID.CustomSQL.sql"
61
+ */
62
+ async function rewriteReferences(filePath) {
63
+ let raw;
64
+ try {
65
+ raw = await readFile(filePath, 'utf8');
66
+ } catch { return; }
67
+
68
+ const updated = raw.replace(/@_output~/g, '@');
69
+ if (updated !== raw) {
70
+ await writeFile(filePath, updated);
71
+ }
72
+ }
73
+
74
+ /**
75
+ * Recursively find directories that contain _output~ files.
76
+ * Skips .dbo/, node_modules/, trash/, .git/.
77
+ */
78
+ async function findDirsWithOutputFiles(root) {
79
+ const SKIP = new Set(['.dbo', 'node_modules', 'trash', '.git', '.claude']);
80
+ const results = [];
81
+
82
+ async function walk(dir) {
83
+ let entries;
84
+ try { entries = await readdir(dir, { withFileTypes: true }); } catch { return; }
85
+
86
+ let hasOutputFiles = false;
87
+ for (const entry of entries) {
88
+ if (entry.isDirectory()) {
89
+ if (!SKIP.has(entry.name)) {
90
+ await walk(join(dir, entry.name));
91
+ }
92
+ } else if (entry.name.startsWith(OUTPUT_PREFIX)) {
93
+ hasOutputFiles = true;
94
+ }
95
+ }
96
+ if (hasOutputFiles) results.push(dir);
97
+ }
98
+
99
+ await walk(root);
100
+ return results;
101
+ }