Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions src/cli/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { fileURLToPath } from "node:url";
import { execSync } from "node:child_process";
import { findProjectRoot } from "../scanner/project-root.js";
import { scanProject } from "../scanner/anatomy-scanner.js";
import { readJSON, writeJSON, readText, writeText } from "../utils/fs-safe.js";
import { readJSON, writeJSON, readText, writeText, safeCopyFile } from "../utils/fs-safe.js";
import { ensureDir } from "../utils/paths.js";
import { isWindows } from "../utils/platform.js";
import { registerProject } from "./registry.js";
Expand Down Expand Up @@ -307,7 +307,7 @@ function writeTemplateFile(templatesDir: string, wolfDir: string, file: string):
const srcPath = path.join(templatesDir, file);
const destPath = path.join(wolfDir, file);
if (fs.existsSync(srcPath)) {
fs.copyFileSync(srcPath, destPath);
safeCopyFile(srcPath, destPath);
} else {
generateTemplate(destPath, file);
}
Expand Down Expand Up @@ -440,7 +440,7 @@ function copyHookScripts(wolfDir: string): void {
for (const file of hookFiles) {
const src = path.join(sourceDir, file);
if (fs.existsSync(src)) {
fs.copyFileSync(src, path.join(hooksDir, file));
safeCopyFile(src, path.join(hooksDir, file));
copiedAny = true;
}
}
Expand Down
22 changes: 11 additions & 11 deletions src/cli/update.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import * as fs from "node:fs";
import * as path from "node:path";
import { fileURLToPath } from "node:url";
import { getRegisteredProjects, registerProject, type RegisteredProject } from "./registry.js";
import { readJSON, writeJSON, readText, writeText } from "../utils/fs-safe.js";
import { readJSON, writeJSON, readText, writeText, safeCopyFile } from "../utils/fs-safe.js";
import { ensureDir } from "../utils/paths.js";

const __filename = fileURLToPath(import.meta.url);
Expand Down Expand Up @@ -170,7 +170,7 @@ async function updateProject(
const srcPath = path.join(templatesDir, file);
const destPath = path.join(wolfDir, file);
if (fs.existsSync(srcPath)) {
fs.copyFileSync(srcPath, destPath);
safeCopyFile(srcPath, destPath);
}
}
console.log(` ✓ Templates updated (${ALWAYS_OVERWRITE.join(", ")})`);
Expand Down Expand Up @@ -250,7 +250,7 @@ function createBackup(wolfDir: string): string {
for (const file of BACKUP_FILES) {
const src = path.join(wolfDir, file);
if (fs.existsSync(src)) {
fs.copyFileSync(src, path.join(backupDir, file));
safeCopyFile(src, path.join(backupDir, file));
}
}

Expand All @@ -264,7 +264,7 @@ function createBackup(wolfDir: string): string {
for (const f of hookFiles) {
const src = path.join(hooksDir, f);
if (fs.statSync(src).isFile()) {
fs.copyFileSync(src, path.join(hooksBackup, f));
safeCopyFile(src, path.join(hooksBackup, f));
}
}
} catch {}
Expand All @@ -276,13 +276,13 @@ function createBackup(wolfDir: string): string {
if (fs.existsSync(claudeSettings)) {
const claudeBackup = path.join(backupDir, ".claude");
ensureDir(claudeBackup);
fs.copyFileSync(claudeSettings, path.join(claudeBackup, "settings.json"));
safeCopyFile(claudeSettings, path.join(claudeBackup, "settings.json"));
}
const claudeRules = path.join(projectRoot, ".claude", "rules", "openwolf.md");
if (fs.existsSync(claudeRules)) {
const rulesBackup = path.join(backupDir, ".claude", "rules");
ensureDir(rulesBackup);
fs.copyFileSync(claudeRules, path.join(rulesBackup, "openwolf.md"));
safeCopyFile(claudeRules, path.join(rulesBackup, "openwolf.md"));
}

return backupDir;
Expand Down Expand Up @@ -342,7 +342,7 @@ function copyHookScripts(wolfDir: string): void {
for (const file of hookFiles) {
const src = path.join(sourceDir, file);
if (fs.existsSync(src)) {
fs.copyFileSync(src, path.join(hooksDir, file));
safeCopyFile(src, path.join(hooksDir, file));
}
}
}
Expand Down Expand Up @@ -443,7 +443,7 @@ export function restoreCommand(backupName?: string): void {
// Restore files
const files = fs.readdirSync(backupDir).filter(f => fs.statSync(path.join(backupDir, f)).isFile());
for (const file of files) {
fs.copyFileSync(path.join(backupDir, file), path.join(wolfDir, file));
safeCopyFile(path.join(backupDir, file), path.join(wolfDir, file));
}

// Restore hooks if present
Expand All @@ -453,7 +453,7 @@ export function restoreCommand(backupName?: string): void {
const hooksDir = path.join(wolfDir, "hooks");
ensureDir(hooksDir);
for (const f of hookFiles) {
fs.copyFileSync(path.join(hooksBackup, f), path.join(hooksDir, f));
safeCopyFile(path.join(hooksBackup, f), path.join(hooksDir, f));
}
}

Expand All @@ -465,13 +465,13 @@ export function restoreCommand(backupName?: string): void {
if (fs.existsSync(settingsBackup)) {
const dest = path.join(projectRoot, ".claude", "settings.json");
ensureDir(path.dirname(dest));
fs.copyFileSync(settingsBackup, dest);
safeCopyFile(settingsBackup, dest);
}
const rulesBackup = path.join(claudeBackup, "rules", "openwolf.md");
if (fs.existsSync(rulesBackup)) {
const dest = path.join(projectRoot, ".claude", "rules", "openwolf.md");
ensureDir(path.dirname(dest));
fs.copyFileSync(rulesBackup, dest);
safeCopyFile(rulesBackup, dest);
}
}

Expand Down
16 changes: 16 additions & 0 deletions src/utils/fs-safe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,19 @@ export function appendText(filePath: string, content: string): void {
}
fs.appendFileSync(filePath, content, "utf-8");
}

// Drop-in replacement for fs.copyFileSync that works around a libuv/9P
// limitation: fs.copyFileSync uses the copy_file_range syscall on Linux,
// which fails with EPERM when writing to EFS-encrypted directories on
// Windows volumes mounted via WSL2 9P. Plain read+write bypasses
// copy_file_range and works in all cases.
export function safeCopyFile(src: string, dest: string): void {
const dir = path.dirname(dest);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
fs.writeFileSync(dest, fs.readFileSync(src));
try {
fs.chmodSync(dest, fs.statSync(src).mode);
} catch {}
}