fix(migrate-v2): skip symlinks in group copyTree

fs.copyFileSync follows symlinks, so a single broken/dangling link in v1
(e.g. .claude-shared.md → /app/CLAUDE.md, a container-side path that
doesn't resolve on the host) crashed the alphabetical traversal with
ENOENT — preventing later folders, including the actual registered
group, from being copied.

Check entry.isSymbolicLink() and skip with a one-line log. v2 uses
composed CLAUDE.md fragments, so v1's container-path symlinks have no v2
meaning and don't need to be carried forward.
This commit is contained in:
Gavriel Cohen
2026-05-02 16:06:45 +00:00
committed by exe.dev user
parent 8181054bdb
commit 7dbedad9bd

View File

@@ -18,7 +18,16 @@ import Database from 'better-sqlite3';
const SKIP_NAMES = new Set(['CLAUDE.md', 'logs', '.git', '.DS_Store', 'node_modules']);
/** Copy a directory tree, skipping SKIP_NAMES. Never overwrites existing files. */
/**
* Copy a directory tree, skipping SKIP_NAMES. Never overwrites existing files.
*
* Symlinks are skipped, not followed: v1 group folders sometimes contain
* container-side paths like `.claude-shared.md → /app/CLAUDE.md` that
* don't resolve on the host. Following them with `fs.copyFileSync` would
* crash ENOENT on a broken target and abort the rest of the traversal.
* v2 uses composed CLAUDE.md fragments anyway — these v1 symlinks have no
* v2 meaning and don't need to be carried forward.
*/
function copyTree(src: string, dst: string): number {
let written = 0;
if (!fs.existsSync(src)) return 0;
@@ -29,6 +38,10 @@ function copyTree(src: string, dst: string): number {
const s = path.join(src, entry.name);
const d = path.join(dst, entry.name);
if (entry.isSymbolicLink()) {
console.log(`SKIP:symlink ${path.relative(process.cwd(), s)}`);
continue;
}
if (entry.isDirectory()) {
written += copyTree(s, d);
continue;