fix: syncDirectory — guard same-path copy and skip nested .git dirs
Two bugs causing 'EACCES: permission denied, copyfile' when source and target are the same path (e.g. wizard with sourceDir == mosaicHome): 1. No same-path guard — syncDirectory tried to copy every file onto itself; git pack files are read-only (0444) so copyFileSync fails. 2. excludeGit only matched top-level .git — nested .git dirs like sources/agent-skills/.git were copied, hitting the same permission issue. Fixes: - Early return when resolve(source) === resolve(target) - Match .git dirs at any depth via dirName and relPath checks - Skip files inside .git/ paths Added file-ops.test.ts with 4 tests covering all cases.
This commit is contained in:
@@ -9,7 +9,7 @@ import {
|
||||
unlinkSync,
|
||||
statSync,
|
||||
} from 'node:fs';
|
||||
import { dirname, join, relative } from 'node:path';
|
||||
import { dirname, join, relative, resolve } from 'node:path';
|
||||
|
||||
const MAX_BACKUPS = 3;
|
||||
|
||||
@@ -68,6 +68,9 @@ export function syncDirectory(
|
||||
target: string,
|
||||
options: { preserve?: string[]; excludeGit?: boolean } = {},
|
||||
): void {
|
||||
// Guard: source and target are the same directory — nothing to sync
|
||||
if (resolve(source) === resolve(target)) return;
|
||||
|
||||
const preserveSet = new Set(options.preserve ?? []);
|
||||
|
||||
// Collect files from source
|
||||
@@ -77,9 +80,10 @@ export function syncDirectory(
|
||||
const stat = statSync(src);
|
||||
if (stat.isDirectory()) {
|
||||
const relPath = relative(relBase, src);
|
||||
const dirName = relPath.split('/').pop() ?? '';
|
||||
|
||||
// Skip .git
|
||||
if (options.excludeGit && relPath === '.git') return;
|
||||
// Skip any .git directory (top-level or nested, e.g. sources/agent-skills/.git)
|
||||
if (options.excludeGit && (dirName === '.git' || relPath.includes('/.git'))) return;
|
||||
|
||||
// Skip preserved paths at top level
|
||||
if (preserveSet.has(relPath) && existsSync(dest)) return;
|
||||
@@ -91,6 +95,9 @@ export function syncDirectory(
|
||||
} else {
|
||||
const relPath = relative(relBase, src);
|
||||
|
||||
// Skip files inside .git directories
|
||||
if (options.excludeGit && relPath.includes('/.git/')) return;
|
||||
|
||||
// Skip preserved files at top level
|
||||
if (preserveSet.has(relPath) && existsSync(dest)) return;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user