@dboio/cli 0.11.4 → 0.13.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.
- package/README.md +126 -3
- package/bin/dbo.js +4 -0
- package/package.json +1 -1
- package/plugins/claude/dbo/.claude-plugin/plugin.json +1 -1
- package/plugins/claude/dbo/commands/dbo.md +65 -244
- package/plugins/claude/dbo/docs/_audit_required/API/all.md +40 -0
- package/plugins/claude/dbo/docs/_audit_required/API/app.md +38 -0
- package/plugins/claude/dbo/docs/_audit_required/API/athenticate.md +26 -0
- package/plugins/claude/dbo/docs/_audit_required/API/cache.md +29 -0
- package/plugins/claude/dbo/docs/_audit_required/API/content.md +14 -0
- package/plugins/claude/dbo/docs/_audit_required/API/data_source.md +28 -0
- package/plugins/claude/dbo/docs/_audit_required/API/email.md +18 -0
- package/plugins/claude/dbo/docs/_audit_required/API/input.md +25 -0
- package/plugins/claude/dbo/docs/_audit_required/API/instance.md +28 -0
- package/plugins/claude/dbo/docs/_audit_required/API/log.md +8 -0
- package/plugins/claude/dbo/docs/_audit_required/API/media.md +12 -0
- package/plugins/claude/dbo/docs/_audit_required/API/output_by_entity.md +12 -0
- package/plugins/claude/dbo/docs/_audit_required/API/upload.md +7 -0
- package/plugins/claude/dbo/docs/_audit_required/dbo-api-syntax.md +1487 -0
- package/plugins/claude/dbo/docs/_audit_required/dbo-problems-code.md +111 -0
- package/plugins/claude/dbo/docs/_audit_required/dbo-problems-performance.md +109 -0
- package/plugins/claude/dbo/docs/_audit_required/dbo-problems-syntax.md +97 -0
- package/plugins/claude/dbo/docs/_audit_required/dbo-product-market.md +119 -0
- package/plugins/claude/dbo/docs/_audit_required/dbo-white-paper.md +125 -0
- package/plugins/claude/dbo/docs/dbo-cheat-sheet.md +323 -0
- package/plugins/claude/dbo/docs/dbo-cli-readme.md +2222 -0
- package/plugins/claude/dbo/docs/dbo-core-entities.md +878 -0
- package/plugins/claude/dbo/docs/dbo-output-customsql.md +677 -0
- package/plugins/claude/dbo/docs/dbo-output-query.md +967 -0
- package/plugins/claude/dbo/skills/cli/SKILL.md +62 -246
- package/src/commands/add.js +366 -62
- package/src/commands/build.js +102 -0
- package/src/commands/clone.js +602 -139
- package/src/commands/diff.js +4 -0
- package/src/commands/init.js +16 -2
- package/src/commands/input.js +3 -1
- package/src/commands/mv.js +12 -4
- package/src/commands/push.js +265 -70
- package/src/commands/rm.js +16 -3
- package/src/commands/run.js +81 -0
- package/src/lib/config.js +39 -0
- package/src/lib/delta.js +7 -1
- package/src/lib/diff.js +24 -2
- package/src/lib/filenames.js +120 -41
- package/src/lib/ignore.js +6 -0
- package/src/lib/input-parser.js +13 -4
- package/src/lib/scripts.js +232 -0
- package/src/migrations/006-remove-uid-companion-filenames.js +181 -0
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
// src/lib/scripts.js
|
|
2
|
+
import { spawn } from 'child_process';
|
|
3
|
+
|
|
4
|
+
const HOOK_NAMES = ['prebuild', 'build', 'postbuild', 'prepush', 'push', 'postpush'];
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Merge scripts.json with scripts.local.json.
|
|
8
|
+
* scripts.local.json keys take precedence over scripts.json at each scope level.
|
|
9
|
+
*
|
|
10
|
+
* @param {object|null} base - Parsed scripts.json (or null)
|
|
11
|
+
* @param {object|null} local - Parsed scripts.local.json (or null)
|
|
12
|
+
* @returns {object} - Merged config with { scripts, targets, entities } keys
|
|
13
|
+
*/
|
|
14
|
+
export function mergeScriptsConfig(base, local) {
|
|
15
|
+
const merged = {
|
|
16
|
+
scripts: Object.assign({}, base?.scripts, local?.scripts),
|
|
17
|
+
targets: {},
|
|
18
|
+
entities: {},
|
|
19
|
+
};
|
|
20
|
+
// Merge target entries: normalize path separators, local overrides base per-hook
|
|
21
|
+
const allTargetKeys = new Set([
|
|
22
|
+
...Object.keys(base?.targets || {}),
|
|
23
|
+
...Object.keys(local?.targets || {}),
|
|
24
|
+
]);
|
|
25
|
+
for (const key of allTargetKeys) {
|
|
26
|
+
const normalKey = key.replace(/\\/g, '/');
|
|
27
|
+
merged.targets[normalKey] = Object.assign(
|
|
28
|
+
{},
|
|
29
|
+
base?.targets?.[key],
|
|
30
|
+
local?.targets?.[key]
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
// Merge entity entries: local overrides base per-hook
|
|
34
|
+
const allEntityKeys = new Set([
|
|
35
|
+
...Object.keys(base?.entities || {}),
|
|
36
|
+
...Object.keys(local?.entities || {}),
|
|
37
|
+
]);
|
|
38
|
+
for (const key of allEntityKeys) {
|
|
39
|
+
merged.entities[key] = Object.assign(
|
|
40
|
+
{},
|
|
41
|
+
base?.entities?.[key],
|
|
42
|
+
local?.entities?.[key]
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
return merged;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Resolve hooks for a given file path and entity type.
|
|
50
|
+
* Resolution order (highest priority first): target → entity → global scripts.
|
|
51
|
+
* Resolution is per-hook — a target can override 'build' while global 'prepush' still applies.
|
|
52
|
+
*
|
|
53
|
+
* @param {string} filePath - Relative file path from project root (forward slashes)
|
|
54
|
+
* @param {string} entityType - Entity type string (e.g., 'content', 'extension.control')
|
|
55
|
+
* @param {object} config - Merged scripts config from mergeScriptsConfig()
|
|
56
|
+
* @returns {object} - Resolved hook map: { prebuild, build, postbuild, prepush, push, postpush }
|
|
57
|
+
* Each value is the resolved hook (string|string[]|false|true|undefined)
|
|
58
|
+
*/
|
|
59
|
+
export function resolveHooks(filePath, entityType, config) {
|
|
60
|
+
const normalPath = filePath.replace(/\\/g, '/');
|
|
61
|
+
const resolved = {};
|
|
62
|
+
|
|
63
|
+
for (const hook of HOOK_NAMES) {
|
|
64
|
+
let value;
|
|
65
|
+
|
|
66
|
+
// 1. Global scripts (lowest priority)
|
|
67
|
+
if (config.scripts && hook in config.scripts) {
|
|
68
|
+
value = config.scripts[hook];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// 2. Entity-level: check both 'extension' (base) and 'extension.control' (specific)
|
|
72
|
+
// More-specific descriptor match overrides base entity match
|
|
73
|
+
const [baseEntity] = entityType.split('.');
|
|
74
|
+
if (baseEntity !== entityType && config.entities[baseEntity] && hook in config.entities[baseEntity]) {
|
|
75
|
+
value = config.entities[baseEntity][hook];
|
|
76
|
+
}
|
|
77
|
+
if (config.entities[entityType] && hook in config.entities[entityType]) {
|
|
78
|
+
value = config.entities[entityType][hook];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// 3. Target-level (highest priority): exact path match OR directory prefix match
|
|
82
|
+
for (const [targetKey, targetHooks] of Object.entries(config.targets)) {
|
|
83
|
+
const isDir = targetKey.endsWith('/');
|
|
84
|
+
const matches = isDir
|
|
85
|
+
? normalPath.startsWith(targetKey) || normalPath.startsWith(targetKey.slice(0, -1) + '/')
|
|
86
|
+
: normalPath === targetKey;
|
|
87
|
+
if (matches && targetHooks && hook in targetHooks) {
|
|
88
|
+
value = targetHooks[hook];
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (value !== undefined) resolved[hook] = value;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return resolved;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Execute a single hook value.
|
|
100
|
+
* - string: run as shell command
|
|
101
|
+
* - string[]: run sequentially, fail-fast on first non-zero exit
|
|
102
|
+
* - false: no-op (returns { skipped: false })
|
|
103
|
+
* - true/undefined: no-op (use default behavior)
|
|
104
|
+
*
|
|
105
|
+
* @param {string|string[]|boolean|undefined} hookValue
|
|
106
|
+
* @param {object} env - Environment variables to inject (merged with process.env)
|
|
107
|
+
* @param {object} [opts]
|
|
108
|
+
* @param {string} [opts.cwd] - Working directory (default: process.cwd())
|
|
109
|
+
* @returns {Promise<{ skipped: boolean, exitCode: number|null }>}
|
|
110
|
+
*/
|
|
111
|
+
export async function runHook(hookValue, env, { cwd = process.cwd() } = {}) {
|
|
112
|
+
if (hookValue === false) return { skipped: false, exitCode: null };
|
|
113
|
+
if (hookValue === true || hookValue === undefined) return { skipped: true, exitCode: null };
|
|
114
|
+
|
|
115
|
+
const commands = Array.isArray(hookValue) ? hookValue : [hookValue];
|
|
116
|
+
const mergedEnv = { ...process.env, ...env };
|
|
117
|
+
|
|
118
|
+
for (const cmd of commands) {
|
|
119
|
+
const exitCode = await spawnShell(cmd, mergedEnv, cwd);
|
|
120
|
+
if (exitCode !== 0) {
|
|
121
|
+
return { skipped: false, exitCode };
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return { skipped: false, exitCode: 0 };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Spawn a shell command and wait for it to exit.
|
|
129
|
+
* Uses /bin/sh -c on Unix, cmd.exe /c on Windows.
|
|
130
|
+
* Inherits stdio so output streams directly to terminal.
|
|
131
|
+
*
|
|
132
|
+
* @param {string} cmd
|
|
133
|
+
* @param {object} env
|
|
134
|
+
* @param {string} cwd
|
|
135
|
+
* @returns {Promise<number>} exit code
|
|
136
|
+
*/
|
|
137
|
+
function spawnShell(cmd, env, cwd) {
|
|
138
|
+
return new Promise((resolve) => {
|
|
139
|
+
const isWin = process.platform === 'win32';
|
|
140
|
+
const shell = isWin ? 'cmd.exe' : '/bin/sh';
|
|
141
|
+
const args = isWin ? ['/c', cmd] : ['-c', cmd];
|
|
142
|
+
const proc = spawn(shell, args, { env, cwd, stdio: 'inherit' });
|
|
143
|
+
proc.on('close', (code) => resolve(code ?? 1));
|
|
144
|
+
proc.on('error', () => resolve(1));
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Build env vars to inject into hook processes for a given target file.
|
|
150
|
+
*
|
|
151
|
+
* @param {string} filePath - Relative file path from project root
|
|
152
|
+
* @param {string} entityType - Entity type string
|
|
153
|
+
* @param {object} appConfig - From loadAppConfig(): { domain, AppShortName, AppUID }
|
|
154
|
+
* @returns {object} env vars
|
|
155
|
+
*/
|
|
156
|
+
export function buildHookEnv(filePath, entityType, appConfig) {
|
|
157
|
+
return {
|
|
158
|
+
DBO_TARGET: filePath,
|
|
159
|
+
DBO_ENTITY: entityType,
|
|
160
|
+
DBO_DOMAIN: appConfig?.domain || '',
|
|
161
|
+
DBO_APP: appConfig?.AppShortName || '',
|
|
162
|
+
DBO_APP_UID: appConfig?.AppUID || '',
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Run the full build lifecycle for a target: prebuild → build → postbuild.
|
|
168
|
+
* Returns false if any hook fails (non-zero exit), true if all succeed or are skipped.
|
|
169
|
+
*
|
|
170
|
+
* @param {object} hooks - Resolved hooks from resolveHooks()
|
|
171
|
+
* @param {object} env - Env vars from buildHookEnv()
|
|
172
|
+
* @param {string} [cwd] - Working directory
|
|
173
|
+
* @returns {Promise<boolean>} true = success/skipped, false = failure
|
|
174
|
+
*/
|
|
175
|
+
export async function runBuildLifecycle(hooks, env, cwd) {
|
|
176
|
+
for (const name of ['prebuild', 'build', 'postbuild']) {
|
|
177
|
+
if (!(name in hooks)) continue;
|
|
178
|
+
const result = await runHook(hooks[name], env, { cwd });
|
|
179
|
+
if (!result.skipped && result.exitCode !== 0 && result.exitCode !== null) {
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return true;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Run the push lifecycle for a target: prepush → (push or default) → postpush.
|
|
188
|
+
* Returns:
|
|
189
|
+
* { runDefault: true } — caller should run the default push
|
|
190
|
+
* { runDefault: false } — push was handled by custom hook or push: false
|
|
191
|
+
* { failed: true } — a hook failed; abort
|
|
192
|
+
*
|
|
193
|
+
* @param {object} hooks - Resolved hooks
|
|
194
|
+
* @param {object} env - Env vars
|
|
195
|
+
* @param {string} [cwd] - Working directory
|
|
196
|
+
*/
|
|
197
|
+
export async function runPushLifecycle(hooks, env, cwd) {
|
|
198
|
+
// prepush
|
|
199
|
+
if ('prepush' in hooks) {
|
|
200
|
+
const result = await runHook(hooks['prepush'], env, { cwd });
|
|
201
|
+
if (!result.skipped && result.exitCode !== 0 && result.exitCode !== null) {
|
|
202
|
+
return { failed: true };
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// push
|
|
207
|
+
let runDefault = true;
|
|
208
|
+
if ('push' in hooks) {
|
|
209
|
+
const pushVal = hooks['push'];
|
|
210
|
+
if (pushVal === false) {
|
|
211
|
+
runDefault = false;
|
|
212
|
+
} else if (pushVal !== true && pushVal !== undefined) {
|
|
213
|
+
// Custom push command overrides default HTTP submit
|
|
214
|
+
const result = await runHook(pushVal, env, { cwd });
|
|
215
|
+
if (!result.skipped && result.exitCode !== 0 && result.exitCode !== null) {
|
|
216
|
+
return { failed: true };
|
|
217
|
+
}
|
|
218
|
+
runDefault = false;
|
|
219
|
+
}
|
|
220
|
+
// push: true or undefined → keep runDefault = true
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// postpush (runs regardless of whether push was default or custom)
|
|
224
|
+
if ('postpush' in hooks) {
|
|
225
|
+
const result = await runHook(hooks['postpush'], env, { cwd });
|
|
226
|
+
if (!result.skipped && result.exitCode !== 0 && result.exitCode !== null) {
|
|
227
|
+
return { failed: true };
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return { runDefault, failed: false };
|
|
232
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { readdir, readFile, writeFile, rename, access, mkdir } from 'fs/promises';
|
|
2
|
+
import { join, extname, basename, dirname } from 'path';
|
|
3
|
+
import { log } from '../lib/logger.js';
|
|
4
|
+
import { stripUidFromFilename, hasUidInFilename } from '../lib/filenames.js';
|
|
5
|
+
import { ensureTrashIcon } from '../lib/folder-icon.js';
|
|
6
|
+
|
|
7
|
+
export const description = 'Rename legacy ~UID companion files to natural filenames';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Migration 006 — Rename companion files from name~uid.ext to name.ext.
|
|
11
|
+
*
|
|
12
|
+
* Handles two cases:
|
|
13
|
+
* A) @reference itself contains ~UID (e.g. "@colors~uid.css") — strip ~UID
|
|
14
|
+
* from both the filename on disk AND the @reference in metadata.
|
|
15
|
+
* B) @reference is already natural but file on disk has ~UID — just rename file.
|
|
16
|
+
*
|
|
17
|
+
* Metadata files retain their ~UID naming.
|
|
18
|
+
*/
|
|
19
|
+
export default async function run(_options) {
|
|
20
|
+
const cwd = process.cwd();
|
|
21
|
+
let totalRenamed = 0;
|
|
22
|
+
let totalRefsUpdated = 0;
|
|
23
|
+
|
|
24
|
+
const metaFiles = await findAllMetadataFiles(cwd);
|
|
25
|
+
|
|
26
|
+
for (const metaPath of metaFiles) {
|
|
27
|
+
try {
|
|
28
|
+
const meta = JSON.parse(await readFile(metaPath, 'utf8'));
|
|
29
|
+
const uid = meta.UID;
|
|
30
|
+
if (!uid) continue;
|
|
31
|
+
|
|
32
|
+
const metaDir = dirname(metaPath);
|
|
33
|
+
const contentCols = [...(meta._contentColumns || [])];
|
|
34
|
+
if (meta._mediaFile) contentCols.push('_mediaFile');
|
|
35
|
+
let metaChanged = false;
|
|
36
|
+
|
|
37
|
+
for (const col of contentCols) {
|
|
38
|
+
const ref = meta[col];
|
|
39
|
+
if (!ref || !String(ref).startsWith('@')) continue;
|
|
40
|
+
|
|
41
|
+
const refName = String(ref).substring(1);
|
|
42
|
+
|
|
43
|
+
// Case A: @reference itself contains ~UID — strip it
|
|
44
|
+
if (hasUidInFilename(refName, uid)) {
|
|
45
|
+
const naturalName = stripUidFromFilename(refName, uid);
|
|
46
|
+
const legacyPath = join(metaDir, refName);
|
|
47
|
+
const naturalPath = join(metaDir, naturalName);
|
|
48
|
+
|
|
49
|
+
// Rename or trash legacy file
|
|
50
|
+
let naturalExists = false;
|
|
51
|
+
let legacyExists = false;
|
|
52
|
+
try { await access(naturalPath); naturalExists = true; } catch { /* missing */ }
|
|
53
|
+
try { await access(legacyPath); legacyExists = true; } catch { /* missing */ }
|
|
54
|
+
|
|
55
|
+
if (legacyExists && !naturalExists) {
|
|
56
|
+
try {
|
|
57
|
+
await rename(legacyPath, naturalPath);
|
|
58
|
+
log.dim(` ${refName} → ${naturalName}`);
|
|
59
|
+
totalRenamed++;
|
|
60
|
+
} catch { /* rename failed */ }
|
|
61
|
+
} else if (legacyExists && naturalExists) {
|
|
62
|
+
// Both exist — move orphan to trash
|
|
63
|
+
try {
|
|
64
|
+
const trashDir = join(process.cwd(), 'trash');
|
|
65
|
+
await mkdir(trashDir, { recursive: true });
|
|
66
|
+
await rename(legacyPath, join(trashDir, basename(legacyPath)));
|
|
67
|
+
await ensureTrashIcon(trashDir);
|
|
68
|
+
log.dim(` Trashed orphan: ${refName}`);
|
|
69
|
+
totalRenamed++;
|
|
70
|
+
} catch { /* non-critical */ }
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Update @reference
|
|
74
|
+
meta[col] = `@${naturalName}`;
|
|
75
|
+
metaChanged = true;
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Case B: @reference is natural but file on disk might have ~UID
|
|
80
|
+
const naturalPath = join(metaDir, refName);
|
|
81
|
+
try { await access(naturalPath); continue; } catch { /* missing */ }
|
|
82
|
+
|
|
83
|
+
const ext = extname(refName);
|
|
84
|
+
const base = basename(refName, ext);
|
|
85
|
+
const legacyName = ext ? `${base}~${uid}${ext}` : `${base}~${uid}`;
|
|
86
|
+
const legacyPath = join(metaDir, legacyName);
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
await access(legacyPath);
|
|
90
|
+
await rename(legacyPath, naturalPath);
|
|
91
|
+
log.dim(` ${legacyName} → ${refName}`);
|
|
92
|
+
totalRenamed++;
|
|
93
|
+
} catch { /* legacy not present — skip */ }
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Rewrite metadata if @references were updated
|
|
97
|
+
if (metaChanged) {
|
|
98
|
+
try {
|
|
99
|
+
await writeFile(metaPath, JSON.stringify(meta, null, 2) + '\n');
|
|
100
|
+
totalRefsUpdated++;
|
|
101
|
+
} catch { /* non-critical */ }
|
|
102
|
+
}
|
|
103
|
+
} catch { /* skip unreadable metadata */ }
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Pass 2: Trash orphaned ~UID companion files not referenced by any metadata
|
|
107
|
+
const referencedFiles = new Set();
|
|
108
|
+
const tildeFiles = [];
|
|
109
|
+
|
|
110
|
+
for (const metaPath of metaFiles) {
|
|
111
|
+
try {
|
|
112
|
+
const meta = JSON.parse(await readFile(metaPath, 'utf8'));
|
|
113
|
+
const metaDir = dirname(metaPath);
|
|
114
|
+
const cols = [...(meta._contentColumns || [])];
|
|
115
|
+
if (meta._mediaFile) cols.push('_mediaFile');
|
|
116
|
+
for (const col of cols) {
|
|
117
|
+
const ref = meta[col];
|
|
118
|
+
if (ref && String(ref).startsWith('@')) {
|
|
119
|
+
const refName = String(ref).substring(1);
|
|
120
|
+
referencedFiles.add(refName.startsWith('/') ? join(cwd, refName) : join(metaDir, refName));
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
} catch { /* skip */ }
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Scan for ~ files not referenced
|
|
127
|
+
async function scanForTildeFiles(dir) {
|
|
128
|
+
let entries;
|
|
129
|
+
try { entries = await readdir(dir, { withFileTypes: true }); } catch { return; }
|
|
130
|
+
for (const entry of entries) {
|
|
131
|
+
if (SKIP.has(entry.name) || entry.name.startsWith('.')) continue;
|
|
132
|
+
const full = join(dir, entry.name);
|
|
133
|
+
if (entry.isDirectory()) { await scanForTildeFiles(full); continue; }
|
|
134
|
+
if (!entry.name.endsWith('.metadata.json') && entry.name.includes('~')) {
|
|
135
|
+
tildeFiles.push(full);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
await scanForTildeFiles(cwd);
|
|
140
|
+
|
|
141
|
+
const orphans = tildeFiles.filter(f => !referencedFiles.has(f));
|
|
142
|
+
if (orphans.length > 0) {
|
|
143
|
+
const trashDir = join(cwd, 'trash');
|
|
144
|
+
await mkdir(trashDir, { recursive: true });
|
|
145
|
+
let trashed = 0;
|
|
146
|
+
for (const orphan of orphans) {
|
|
147
|
+
try {
|
|
148
|
+
await rename(orphan, join(trashDir, basename(orphan)));
|
|
149
|
+
trashed++;
|
|
150
|
+
} catch { /* non-critical */ }
|
|
151
|
+
}
|
|
152
|
+
if (trashed > 0) {
|
|
153
|
+
await ensureTrashIcon(trashDir);
|
|
154
|
+
totalRenamed += trashed;
|
|
155
|
+
log.dim(` Trashed ${trashed} orphaned legacy ~UID file(s)`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (totalRenamed > 0 || totalRefsUpdated > 0) {
|
|
160
|
+
log.dim(` Renamed ${totalRenamed} companion file(s), updated ${totalRefsUpdated} metadata @reference(s)`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const SKIP = new Set(['.dbo', 'node_modules', 'trash', '.git', '.claude']);
|
|
165
|
+
|
|
166
|
+
async function findAllMetadataFiles(dir) {
|
|
167
|
+
const results = [];
|
|
168
|
+
try {
|
|
169
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
170
|
+
for (const entry of entries) {
|
|
171
|
+
if (SKIP.has(entry.name)) continue;
|
|
172
|
+
const full = join(dir, entry.name);
|
|
173
|
+
if (entry.isDirectory()) {
|
|
174
|
+
results.push(...await findAllMetadataFiles(full));
|
|
175
|
+
} else if (entry.name.endsWith('.metadata.json')) {
|
|
176
|
+
results.push(full);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
} catch { /* skip unreadable dirs */ }
|
|
180
|
+
return results;
|
|
181
|
+
}
|