@dboio/cli 0.17.0 → 0.19.1
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 +111 -85
- package/package.json +1 -1
- package/plugins/claude/dbo/docs/dbo-cli-readme.md +111 -85
- package/src/commands/build.js +3 -3
- package/src/commands/clone.js +236 -97
- package/src/commands/deploy.js +3 -3
- package/src/commands/init.js +11 -11
- package/src/commands/install.js +3 -3
- package/src/commands/login.js +2 -2
- package/src/commands/mv.js +15 -15
- package/src/commands/pull.js +1 -1
- package/src/commands/push.js +193 -14
- package/src/commands/rm.js +2 -2
- package/src/commands/run.js +4 -4
- package/src/commands/status.js +1 -1
- package/src/commands/sync.js +2 -2
- package/src/lib/config.js +186 -135
- package/src/lib/delta.js +119 -17
- package/src/lib/dependencies.js +51 -24
- package/src/lib/deploy-config.js +4 -4
- package/src/lib/domain-guard.js +8 -9
- package/src/lib/filenames.js +12 -1
- package/src/lib/ignore.js +2 -3
- package/src/lib/insert.js +1 -1
- package/src/lib/metadata-schema.js +14 -20
- package/src/lib/metadata-templates.js +4 -4
- package/src/lib/migrations.js +1 -1
- package/src/lib/modify-key.js +1 -1
- package/src/lib/scaffold.js +5 -12
- package/src/lib/schema.js +67 -37
- package/src/lib/structure.js +6 -6
- package/src/lib/tagging.js +2 -2
- package/src/lib/ticketing.js +3 -7
- package/src/lib/toe-stepping.js +5 -5
- package/src/lib/transaction-key.js +1 -1
- package/src/migrations/004-rename-output-files.js +2 -2
- package/src/migrations/005-rename-output-metadata.js +2 -2
- package/src/migrations/006-remove-uid-companion-filenames.js +1 -1
- package/src/migrations/007-natural-entity-companion-filenames.js +1 -1
- package/src/migrations/008-metadata-uid-in-suffix.js +1 -1
- package/src/migrations/009-fix-media-collision-metadata-names.js +1 -1
- package/src/migrations/010-delete-paren-media-orphans.js +1 -1
- package/src/migrations/012-project-dir-restructure.js +211 -0
package/src/lib/schema.js
CHANGED
|
@@ -1,53 +1,83 @@
|
|
|
1
|
-
import { readFile
|
|
2
|
-
import {
|
|
1
|
+
import { readFile } from 'fs/promises';
|
|
2
|
+
import { join, sep } from 'path';
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
const SCHEMA_API_PATH = '/api/app/object/_system';
|
|
4
|
+
const SYSTEM_BASELINE_PATH = join('app_dependencies', '_system', '.app', '_system.json');
|
|
6
5
|
|
|
7
|
-
/**
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
6
|
+
/**
|
|
7
|
+
* Kept for call-site compatibility with commands that pass systemSchemaPath.
|
|
8
|
+
* Points to the _system dependency baseline location.
|
|
9
|
+
*/
|
|
10
|
+
export const SCHEMA_FILE = SYSTEM_BASELINE_PATH;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Load schema from the _system dependency baseline.
|
|
14
|
+
* When inside a dependency checkout (app_dependencies/<dep>/), traverses up
|
|
15
|
+
* to the parent project's _system baseline.
|
|
16
|
+
* Returns parsed object or null if the dependency hasn't been cloned yet.
|
|
17
|
+
*/
|
|
18
|
+
export async function loadSchema() {
|
|
19
|
+
// Try local path first (works for top-level projects)
|
|
20
|
+
try {
|
|
21
|
+
return JSON.parse(await readFile(SYSTEM_BASELINE_PATH, 'utf8'));
|
|
22
|
+
} catch { /* not found locally */ }
|
|
23
|
+
|
|
24
|
+
// If inside a dependency checkout, try parent project's schema
|
|
25
|
+
const cwd = process.cwd();
|
|
26
|
+
const marker = `${sep}app_dependencies${sep}`;
|
|
27
|
+
const depIdx = cwd.indexOf(marker);
|
|
28
|
+
if (depIdx >= 0) {
|
|
29
|
+
const parentRoot = cwd.substring(0, depIdx);
|
|
30
|
+
const parentSchemaPath = join(parentRoot, SYSTEM_BASELINE_PATH);
|
|
31
|
+
try {
|
|
32
|
+
return JSON.parse(await readFile(parentSchemaPath, 'utf8'));
|
|
33
|
+
} catch { /* parent schema not available either */ }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return null;
|
|
13
37
|
}
|
|
14
38
|
|
|
15
39
|
/**
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* Returns ISO date string or null on failure.
|
|
40
|
+
* saveSchema() is now a no-op — schema is managed by the _system dependency clone.
|
|
41
|
+
* Kept for call-site compatibility.
|
|
19
42
|
*/
|
|
20
|
-
export async function
|
|
21
|
-
|
|
22
|
-
const result = await client.get(`${SCHEMA_API_PATH}[_LastUpdated]`);
|
|
23
|
-
if (!result.ok || !result.data) return null;
|
|
24
|
-
return result.data._LastUpdated || null;
|
|
43
|
+
export async function saveSchema(_data) {
|
|
44
|
+
// no-op
|
|
25
45
|
}
|
|
26
46
|
|
|
27
47
|
/**
|
|
28
|
-
* Check whether the
|
|
29
|
-
*
|
|
48
|
+
* Check whether the _system dependency needs refreshing.
|
|
49
|
+
* Delegates to checkDependencyStaleness('_system').
|
|
30
50
|
*/
|
|
31
51
|
export async function isSchemaStale(options = {}) {
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
const serverTs = await fetchSchemaLastUpdated(options);
|
|
36
|
-
if (!serverTs) return false; // can't determine — assume not stale
|
|
37
|
-
|
|
38
|
-
return new Date(serverTs) > new Date(local._LastUpdated);
|
|
52
|
+
const { checkDependencyStaleness } = await import('./dependencies.js');
|
|
53
|
+
return checkDependencyStaleness('_system', options);
|
|
39
54
|
}
|
|
40
55
|
|
|
41
|
-
/**
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
56
|
+
/**
|
|
57
|
+
* "Fetch" schema by forcing a re-clone of the _system dependency.
|
|
58
|
+
* When inside a dependency checkout, falls back to direct API fetch
|
|
59
|
+
* (syncDependencies would skip due to the self-detection guard).
|
|
60
|
+
* Returns the freshly loaded schema after sync completes.
|
|
61
|
+
*/
|
|
62
|
+
export async function fetchSchema(options = {}) {
|
|
63
|
+
const cwd = process.cwd();
|
|
64
|
+
if (cwd.includes(`${sep}app_dependencies${sep}`)) {
|
|
65
|
+
// Inside a dependency checkout — can't sync _system here.
|
|
66
|
+
// Try parent's schema first (already cloned by parent).
|
|
67
|
+
const existing = await loadSchema();
|
|
68
|
+
if (existing) return existing;
|
|
69
|
+
|
|
70
|
+
// Fallback: fetch directly from API (old behavior)
|
|
71
|
+
const { DboClient } = await import('./client.js');
|
|
72
|
+
const { loadConfig } = await import('./config.js');
|
|
73
|
+
const config = await loadConfig();
|
|
74
|
+
const domain = options.domain || config.domain;
|
|
75
|
+
const client = new DboClient({ domain, verbose: options.verbose });
|
|
76
|
+
const resp = await client.get('/api/app/object/_system');
|
|
77
|
+
return resp?.data || resp;
|
|
47
78
|
}
|
|
48
|
-
}
|
|
49
79
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
80
|
+
const { syncDependencies } = await import('./dependencies.js');
|
|
81
|
+
await syncDependencies({ ...options, only: ['_system'], force: true });
|
|
82
|
+
return loadSchema();
|
|
53
83
|
}
|
package/src/lib/structure.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { readFile, writeFile, mkdir } from 'fs/promises';
|
|
2
2
|
import { join } from 'path';
|
|
3
3
|
|
|
4
|
-
const STRUCTURE_FILE = '.
|
|
4
|
+
const STRUCTURE_FILE = '.app/directories.json';
|
|
5
5
|
|
|
6
6
|
/** All server-managed directories live under this subdirectory */
|
|
7
7
|
export const LIB_DIR = 'lib';
|
|
@@ -271,15 +271,15 @@ export async function createDirectories(structure) {
|
|
|
271
271
|
}
|
|
272
272
|
|
|
273
273
|
/**
|
|
274
|
-
* Save the bin structure to .
|
|
274
|
+
* Save the bin structure to .app/directories.json.
|
|
275
275
|
*/
|
|
276
276
|
export async function saveStructureFile(structure) {
|
|
277
|
-
await mkdir('.
|
|
277
|
+
await mkdir('.app', { recursive: true });
|
|
278
278
|
await writeFile(STRUCTURE_FILE, JSON.stringify(structure, null, 2) + '\n');
|
|
279
279
|
}
|
|
280
280
|
|
|
281
281
|
/**
|
|
282
|
-
* Load bin structure from .
|
|
282
|
+
* Load bin structure from .app/directories.json.
|
|
283
283
|
*/
|
|
284
284
|
export async function loadStructureFile() {
|
|
285
285
|
try {
|
|
@@ -389,7 +389,7 @@ export function resolveFieldValue(value) {
|
|
|
389
389
|
}
|
|
390
390
|
|
|
391
391
|
/**
|
|
392
|
-
* Persist descriptorMapping and extensionDescriptorDirs into .
|
|
392
|
+
* Persist descriptorMapping and extensionDescriptorDirs into .app/directories.json.
|
|
393
393
|
* Extends the existing structure object (already contains bin entries).
|
|
394
394
|
*
|
|
395
395
|
* @param {Object} structure - Current structure from loadStructureFile()
|
|
@@ -411,7 +411,7 @@ export async function saveDescriptorMapping(structure, mapping) {
|
|
|
411
411
|
}
|
|
412
412
|
|
|
413
413
|
/**
|
|
414
|
-
* Load descriptorMapping from .
|
|
414
|
+
* Load descriptorMapping from .app/directories.json.
|
|
415
415
|
* Returns {} if not yet persisted.
|
|
416
416
|
*/
|
|
417
417
|
export async function loadDescriptorMapping() {
|
package/src/lib/tagging.js
CHANGED
|
@@ -149,13 +149,13 @@ async function _findUntrackedFiles(dir, ig, knownMetaPaths) {
|
|
|
149
149
|
return all.filter(fp => !knownCompanions.has(fp));
|
|
150
150
|
}
|
|
151
151
|
|
|
152
|
-
// Recursively collect non-metadata, non-hidden, non-.
|
|
152
|
+
// Recursively collect non-metadata, non-hidden, non-.app content files
|
|
153
153
|
async function _collectContentFiles(dir, ig) {
|
|
154
154
|
const results = [];
|
|
155
155
|
let entries;
|
|
156
156
|
try { entries = await readdir(dir, { withFileTypes: true }); } catch { return []; }
|
|
157
157
|
for (const entry of entries) {
|
|
158
|
-
if (entry.name.startsWith('.')) continue; // skip hidden and .
|
|
158
|
+
if (entry.name.startsWith('.')) continue; // skip hidden and .app
|
|
159
159
|
const fullPath = join(dir, entry.name);
|
|
160
160
|
const relPath = relative(process.cwd(), fullPath).replace(/\\/g, '/');
|
|
161
161
|
if (entry.isDirectory()) {
|
package/src/lib/ticketing.js
CHANGED
|
@@ -1,16 +1,12 @@
|
|
|
1
1
|
import { readFile, writeFile, mkdir } from 'fs/promises';
|
|
2
2
|
import { join } from 'path';
|
|
3
3
|
import { log } from './logger.js';
|
|
4
|
+
import { projectDir } from './config.js';
|
|
4
5
|
|
|
5
|
-
const DBO_DIR = '.dbo';
|
|
6
6
|
const TICKETING_FILE = 'ticketing.local.json';
|
|
7
7
|
|
|
8
|
-
function dboDir() {
|
|
9
|
-
return join(process.cwd(), DBO_DIR);
|
|
10
|
-
}
|
|
11
|
-
|
|
12
8
|
function ticketingPath() {
|
|
13
|
-
return join(
|
|
9
|
+
return join(projectDir(), TICKETING_FILE);
|
|
14
10
|
}
|
|
15
11
|
|
|
16
12
|
const DEFAULT_TICKETING = { ticket_id: null, ticketing_required: false, ticket_confirmed: false, records: [] };
|
|
@@ -48,7 +44,7 @@ export async function loadTicketing() {
|
|
|
48
44
|
* Save ticketing.local.json.
|
|
49
45
|
*/
|
|
50
46
|
export async function saveTicketing(data) {
|
|
51
|
-
await mkdir(
|
|
47
|
+
await mkdir(projectDir(), { recursive: true });
|
|
52
48
|
await writeFile(ticketingPath(), JSON.stringify(data, null, 2) + '\n');
|
|
53
49
|
}
|
|
54
50
|
|
package/src/lib/toe-stepping.js
CHANGED
|
@@ -87,8 +87,8 @@ export async function fetchServerRecords(client, requests) {
|
|
|
87
87
|
* makes a single HTTP request and the server only returns records modified
|
|
88
88
|
* after the given date.
|
|
89
89
|
*
|
|
90
|
-
* The response is used ONLY for comparison — it must NOT replace
|
|
91
|
-
*
|
|
90
|
+
* The response is used ONLY for comparison — it must NOT replace the metadata
|
|
91
|
+
* or baseline files.
|
|
92
92
|
*
|
|
93
93
|
* @param {DboClient} client
|
|
94
94
|
* @param {string} appShortName - App short name for the /api/app/object/ endpoint
|
|
@@ -181,7 +181,7 @@ function decodeServerValue(value) {
|
|
|
181
181
|
* a newer server edit, and _LastUpdatedUserID identifies who made the change.
|
|
182
182
|
*
|
|
183
183
|
* @param {Object} serverEntry - Record from per-record server fetch
|
|
184
|
-
* @param {Object} baselineEntry - Record from
|
|
184
|
+
* @param {Object} baselineEntry - Record from the baseline file
|
|
185
185
|
* @param {Object} localMeta - Local .metadata.json object
|
|
186
186
|
* @param {string} metaDir - Absolute directory of the metadata file
|
|
187
187
|
* @returns {Promise<Array<{ col: string, serverValue: string, localValue: string, baselineValue: string }>>}
|
|
@@ -278,12 +278,12 @@ function findOldestBaselineDate(records, baseline) {
|
|
|
278
278
|
* unavailable or returns no data.
|
|
279
279
|
*
|
|
280
280
|
* The bulk response is used ONLY for comparison — it does NOT replace
|
|
281
|
-
*
|
|
281
|
+
* the metadata or baseline files.
|
|
282
282
|
*
|
|
283
283
|
* @param {Array<{ meta: Object, metaPath: string }>} records
|
|
284
284
|
* Records about to be pushed. Each must have meta.UID, meta._entity.
|
|
285
285
|
* @param {DboClient} client
|
|
286
|
-
* @param {Object} baseline - Loaded baseline from .
|
|
286
|
+
* @param {Object} baseline - Loaded baseline from .app/<shortName>.json (baseline)
|
|
287
287
|
* @param {Object} options - Commander options (options.yes used for auto-accept)
|
|
288
288
|
* @param {string} [appShortName] - App short name for bulk fetch (optional)
|
|
289
289
|
* @param {string} [serverTz] - Server timezone from config (e.g. "America/Chicago")
|
|
@@ -41,6 +41,6 @@ export async function resolveTransactionKey(options = {}) {
|
|
|
41
41
|
}]);
|
|
42
42
|
|
|
43
43
|
await saveTransactionKeyPreset(preset);
|
|
44
|
-
log.dim(` Saved TransactionKeyPreset: ${preset} to .
|
|
44
|
+
log.dim(` Saved TransactionKeyPreset: ${preset} to .app/config.json`);
|
|
45
45
|
return preset;
|
|
46
46
|
}
|
|
@@ -73,10 +73,10 @@ async function rewriteReferences(filePath) {
|
|
|
73
73
|
|
|
74
74
|
/**
|
|
75
75
|
* Recursively find directories that contain _output~ files.
|
|
76
|
-
* Skips .
|
|
76
|
+
* Skips .app/, node_modules/, trash/, .git/.
|
|
77
77
|
*/
|
|
78
78
|
async function findDirsWithOutputFiles(root) {
|
|
79
|
-
const SKIP = new Set(['.
|
|
79
|
+
const SKIP = new Set(['.app', 'node_modules', 'trash', '.git', '.claude', 'app_dependencies']);
|
|
80
80
|
const results = [];
|
|
81
81
|
|
|
82
82
|
async function walk(dir) {
|
|
@@ -57,10 +57,10 @@ export default async function run(_options) {
|
|
|
57
57
|
|
|
58
58
|
/**
|
|
59
59
|
* Recursively find directories that contain output .json files (with ~ in name).
|
|
60
|
-
* Skips .
|
|
60
|
+
* Skips .app/, node_modules/, trash/, .git/.
|
|
61
61
|
*/
|
|
62
62
|
async function findDirsWithOutputJsonFiles(root) {
|
|
63
|
-
const SKIP = new Set(['.
|
|
63
|
+
const SKIP = new Set(['.app', 'node_modules', 'trash', '.git', '.claude', 'app_dependencies']);
|
|
64
64
|
const results = [];
|
|
65
65
|
|
|
66
66
|
async function walk(dir) {
|
|
@@ -161,7 +161,7 @@ export default async function run(_options) {
|
|
|
161
161
|
}
|
|
162
162
|
}
|
|
163
163
|
|
|
164
|
-
const SKIP = new Set(['.
|
|
164
|
+
const SKIP = new Set(['.app', 'node_modules', 'trash', '.git', '.claude', 'app_dependencies']);
|
|
165
165
|
|
|
166
166
|
async function findAllMetadataFiles(dir) {
|
|
167
167
|
const results = [];
|
|
@@ -127,7 +127,7 @@ const ENTITY_DIRS = new Set([
|
|
|
127
127
|
'data_source', 'group', 'site', 'redirect',
|
|
128
128
|
]);
|
|
129
129
|
|
|
130
|
-
const SKIP = new Set(['.
|
|
130
|
+
const SKIP = new Set(['.app', 'node_modules', 'trash', '.git', '.claude', 'app_dependencies']);
|
|
131
131
|
|
|
132
132
|
/**
|
|
133
133
|
* Find all .metadata.json files inside entity directories (lib/<entity>/).
|
|
@@ -50,7 +50,7 @@ export default async function run(_options) {
|
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
const SKIP = new Set(['.
|
|
53
|
+
const SKIP = new Set(['.app', 'node_modules', 'trash', '.git', '.claude', 'app_dependencies']);
|
|
54
54
|
|
|
55
55
|
async function findAllLegacyMetadataFiles(dir) {
|
|
56
56
|
const results = [];
|
|
@@ -141,7 +141,7 @@ export default async function run(_options) {
|
|
|
141
141
|
}
|
|
142
142
|
}
|
|
143
143
|
|
|
144
|
-
const SKIP = new Set(['.
|
|
144
|
+
const SKIP = new Set(['.app', 'node_modules', 'trash', '.git', '.claude', 'app_dependencies']);
|
|
145
145
|
|
|
146
146
|
async function findAllMetadataFiles(dir) {
|
|
147
147
|
const results = [];
|
|
@@ -41,7 +41,7 @@ export default async function run(_options) {
|
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
const SKIP = new Set(['.
|
|
44
|
+
const SKIP = new Set(['.app', 'node_modules', 'trash', '.git', '.claude', 'app_dependencies']);
|
|
45
45
|
|
|
46
46
|
async function findParenMediaFiles(dir) {
|
|
47
47
|
const results = [];
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { readFile, writeFile, rename, mkdir, rm, readdir, access } from 'fs/promises';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { homedir } from 'os';
|
|
4
|
+
|
|
5
|
+
export const description = 'Rename .dbo/ → .app/; rename baseline/structure/metadata files; move dependencies → app_dependencies/; move config.local.json → ~/.dbo/settings.json; remove schema.json';
|
|
6
|
+
|
|
7
|
+
async function fileExists(p) {
|
|
8
|
+
try { await access(p); return true; } catch { return false; }
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export default async function run() {
|
|
12
|
+
const cwd = process.cwd();
|
|
13
|
+
const log = (...args) => console.log(...args);
|
|
14
|
+
|
|
15
|
+
const dboDirPath = join(cwd, '.dbo');
|
|
16
|
+
const appDirPath = join(cwd, '.app');
|
|
17
|
+
|
|
18
|
+
// Guard: skip if already on new layout
|
|
19
|
+
if (!(await fileExists(dboDirPath)) || (await fileExists(appDirPath))) {
|
|
20
|
+
log(' .dbo/ not found or .app/ already exists — skipping');
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Step 1: Read AppShortName from .dbo/config.json BEFORE rename
|
|
25
|
+
let appShortName = null;
|
|
26
|
+
try {
|
|
27
|
+
const cfg = JSON.parse(await readFile(join(dboDirPath, 'config.json'), 'utf8'));
|
|
28
|
+
appShortName = cfg.AppShortName || null;
|
|
29
|
+
} catch { /* no config — proceed without AppShortName */ }
|
|
30
|
+
|
|
31
|
+
const safeName = appShortName
|
|
32
|
+
? String(appShortName).replace(/[/\\:*?"<>|]/g, '-')
|
|
33
|
+
: null;
|
|
34
|
+
|
|
35
|
+
// Step 2: Rename .dbo/ → .app/
|
|
36
|
+
try {
|
|
37
|
+
await rename(dboDirPath, appDirPath);
|
|
38
|
+
log(` Renamed .dbo/ → .app/`);
|
|
39
|
+
} catch (err) {
|
|
40
|
+
log(` (skip) Could not rename .dbo/ → .app/: ${err.message}`);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Step 3: Rename .app/.app_baseline.json → .app/<shortName>.json
|
|
45
|
+
if (safeName) {
|
|
46
|
+
const oldBaseline = join(appDirPath, '.app_baseline.json');
|
|
47
|
+
const newBaseline = join(appDirPath, `${safeName}.json`);
|
|
48
|
+
if ((await fileExists(oldBaseline)) && !(await fileExists(newBaseline))) {
|
|
49
|
+
try {
|
|
50
|
+
await rename(oldBaseline, newBaseline);
|
|
51
|
+
log(` Renamed .app/.app_baseline.json → .app/${safeName}.json`);
|
|
52
|
+
} catch (e) { log(` (skip) baseline rename: ${e.message}`); }
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Step 4: Rename .app/structure.json → .app/directories.json
|
|
57
|
+
const oldStructure = join(appDirPath, 'structure.json');
|
|
58
|
+
const newStructure = join(appDirPath, 'directories.json');
|
|
59
|
+
if ((await fileExists(oldStructure)) && !(await fileExists(newStructure))) {
|
|
60
|
+
try {
|
|
61
|
+
await rename(oldStructure, newStructure);
|
|
62
|
+
log(` Renamed .app/structure.json → .app/directories.json`);
|
|
63
|
+
} catch (e) { log(` (skip) structure rename: ${e.message}`); }
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Step 4b: Rename .app/metadata_schema.json → .app/<shortName>.metadata_schema.json
|
|
67
|
+
if (safeName) {
|
|
68
|
+
const oldSchema = join(appDirPath, 'metadata_schema.json');
|
|
69
|
+
const newSchema = join(appDirPath, `${safeName}.metadata_schema.json`);
|
|
70
|
+
if ((await fileExists(oldSchema)) && !(await fileExists(newSchema))) {
|
|
71
|
+
try {
|
|
72
|
+
await rename(oldSchema, newSchema);
|
|
73
|
+
log(` Renamed .app/metadata_schema.json → .app/${safeName}.metadata_schema.json`);
|
|
74
|
+
} catch (e) { log(` (skip) metadata_schema rename: ${e.message}`); }
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Step 5: Move .app/dependencies/ → app_dependencies/
|
|
79
|
+
const oldDepsDir = join(appDirPath, 'dependencies');
|
|
80
|
+
const newDepsRoot = join(cwd, 'app_dependencies');
|
|
81
|
+
if (await fileExists(oldDepsDir)) {
|
|
82
|
+
try {
|
|
83
|
+
await mkdir(newDepsRoot, { recursive: true });
|
|
84
|
+
const depNames = await readdir(oldDepsDir);
|
|
85
|
+
for (const name of depNames) {
|
|
86
|
+
const src = join(oldDepsDir, name);
|
|
87
|
+
const dest = join(newDepsRoot, name);
|
|
88
|
+
if (!(await fileExists(dest))) {
|
|
89
|
+
await rename(src, dest);
|
|
90
|
+
log(` Moved app_dependencies/${name}`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
try { await rm(oldDepsDir, { recursive: true }); } catch { /* non-empty */ }
|
|
94
|
+
} catch (e) { log(` (skip) dependencies move: ${e.message}`); }
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Step 6: Move root app.json → .app/<shortName>.metadata.json
|
|
98
|
+
if (safeName) {
|
|
99
|
+
const rootAppJson = join(cwd, 'app.json');
|
|
100
|
+
const newMetaPath = join(appDirPath, `${safeName}.metadata.json`);
|
|
101
|
+
if ((await fileExists(rootAppJson)) && !(await fileExists(newMetaPath))) {
|
|
102
|
+
try {
|
|
103
|
+
await rename(rootAppJson, newMetaPath);
|
|
104
|
+
log(` Moved app.json → .app/${safeName}.metadata.json`);
|
|
105
|
+
} catch (e) { log(` (skip) app.json move: ${e.message}`); }
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Step 7: Remove root schema.json
|
|
110
|
+
const rootSchema = join(cwd, 'schema.json');
|
|
111
|
+
if (await fileExists(rootSchema)) {
|
|
112
|
+
try {
|
|
113
|
+
await rm(rootSchema);
|
|
114
|
+
log(` Removed schema.json (schema now sourced from app_dependencies/_system/.app/_system.json)`);
|
|
115
|
+
} catch (e) { log(` (skip) schema.json removal: ${e.message}`); }
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Step 8: Move .app/config.local.json → ~/.dbo/settings.json
|
|
119
|
+
const oldLocal = join(appDirPath, 'config.local.json');
|
|
120
|
+
const globalSettingsDir = join(homedir(), '.dbo');
|
|
121
|
+
const globalSettings = join(globalSettingsDir, 'settings.json');
|
|
122
|
+
if (await fileExists(oldLocal)) {
|
|
123
|
+
try {
|
|
124
|
+
await mkdir(globalSettingsDir, { recursive: true });
|
|
125
|
+
let oldData = {};
|
|
126
|
+
try { oldData = JSON.parse(await readFile(oldLocal, 'utf8')); } catch { /* ignore */ }
|
|
127
|
+
|
|
128
|
+
let newData = {};
|
|
129
|
+
try { newData = JSON.parse(await readFile(globalSettings, 'utf8')); } catch { /* new file */ }
|
|
130
|
+
|
|
131
|
+
if (oldData.plugins) {
|
|
132
|
+
if (!newData.plugins) newData.plugins = {};
|
|
133
|
+
Object.assign(newData.plugins, oldData.plugins);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (Array.isArray(oldData._completedMigrations) && oldData._completedMigrations.length > 0) {
|
|
137
|
+
if (!newData._completedMigrations) newData._completedMigrations = {};
|
|
138
|
+
const existing = new Set(Array.isArray(newData._completedMigrations[cwd]) ? newData._completedMigrations[cwd] : []);
|
|
139
|
+
for (const id of oldData._completedMigrations) existing.add(id);
|
|
140
|
+
newData._completedMigrations[cwd] = [...existing].sort();
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
await writeFile(globalSettings, JSON.stringify(newData, null, 2) + '\n');
|
|
144
|
+
try { await rm(oldLocal); } catch { /* leave it */ }
|
|
145
|
+
log(` Merged .app/config.local.json → ~/.dbo/settings.json`);
|
|
146
|
+
} catch (e) { log(` (skip) config.local.json merge: ${e.message}`); }
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Step 9: Update .gitignore
|
|
150
|
+
const gitignorePath = join(cwd, '.gitignore');
|
|
151
|
+
try {
|
|
152
|
+
let content = '';
|
|
153
|
+
try { content = await readFile(gitignorePath, 'utf8'); } catch { /* no .gitignore */ }
|
|
154
|
+
|
|
155
|
+
const replacements = [
|
|
156
|
+
['.dbo/credentials.json', '.app/credentials.json'],
|
|
157
|
+
['.dbo/cookies.txt', '.app/cookies.txt'],
|
|
158
|
+
['.dbo/config.local.json', null],
|
|
159
|
+
['.dbo/.app_baseline.json', null],
|
|
160
|
+
['.dbo/ticketing.local.json', '.app/ticketing.local.json'],
|
|
161
|
+
['.dbo/scripts.local.json', '.app/scripts.local.json'],
|
|
162
|
+
['.dbo/errors.log', '.app/errors.log'],
|
|
163
|
+
['.dbo/dependencies/', null],
|
|
164
|
+
['schema.json', null],
|
|
165
|
+
];
|
|
166
|
+
|
|
167
|
+
let changed = false;
|
|
168
|
+
let lines = content.split('\n');
|
|
169
|
+
|
|
170
|
+
for (const [oldPat, newPat] of replacements) {
|
|
171
|
+
const idx = lines.findIndex(l => l.trim() === oldPat);
|
|
172
|
+
if (idx >= 0) {
|
|
173
|
+
if (newPat) { lines[idx] = newPat; }
|
|
174
|
+
else lines.splice(idx, 1);
|
|
175
|
+
changed = true;
|
|
176
|
+
} else if (newPat && !content.includes(newPat)) {
|
|
177
|
+
lines.push(newPat);
|
|
178
|
+
changed = true;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (!content.includes('app_dependencies/')) {
|
|
183
|
+
lines.push('app_dependencies/');
|
|
184
|
+
changed = true;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (changed) {
|
|
188
|
+
await writeFile(gitignorePath, lines.join('\n'));
|
|
189
|
+
log(` Updated .gitignore with new .app/ paths`);
|
|
190
|
+
}
|
|
191
|
+
} catch (e) { log(` (skip) .gitignore update: ${e.message}`); }
|
|
192
|
+
|
|
193
|
+
// Step 10: Update .dboignore
|
|
194
|
+
const dboignorePath = join(cwd, '.dboignore');
|
|
195
|
+
try {
|
|
196
|
+
let content = await readFile(dboignorePath, 'utf8');
|
|
197
|
+
const updated = content
|
|
198
|
+
.replace(/^\.dbo\/$/m, '.app/')
|
|
199
|
+
.replace(/^\.dbo\/dependencies\/$/m, '')
|
|
200
|
+
.replace(/^app\.json$/m, '');
|
|
201
|
+
if (!updated.includes('app_dependencies/')) {
|
|
202
|
+
const finalContent = updated.replace(/^\.app\/$/m, '.app/\napp_dependencies/');
|
|
203
|
+
await writeFile(dboignorePath, finalContent);
|
|
204
|
+
} else {
|
|
205
|
+
await writeFile(dboignorePath, updated);
|
|
206
|
+
}
|
|
207
|
+
log(` Updated .dboignore`);
|
|
208
|
+
} catch { /* no .dboignore */ }
|
|
209
|
+
|
|
210
|
+
log(` Migration 012 complete: .dbo/ → .app/`);
|
|
211
|
+
}
|