import { promises as fs } from "fs" import path from "path" export async function backupFile(filePath: string): Promise { if (!(await pathExists(filePath))) return null try { const timestamp = new Date().toISOString().replace(/[:.]/g, "-") const backupPath = `${filePath}.bak.${timestamp}` await fs.copyFile(filePath, backupPath) return backupPath } catch { return null } } export async function pathExists(filePath: string): Promise { try { await fs.access(filePath) return true } catch { return false } } export async function ensureDir(dirPath: string): Promise { await fs.mkdir(dirPath, { recursive: true }) } export async function readText(filePath: string): Promise { return fs.readFile(filePath, "utf8") } export async function readJson(filePath: string): Promise { const raw = await readText(filePath) return JSON.parse(raw) as T } export async function writeText(filePath: string, content: string): Promise { await ensureDir(path.dirname(filePath)) await fs.writeFile(filePath, content, "utf8") } export async function writeTextSecure(filePath: string, content: string): Promise { await ensureDir(path.dirname(filePath)) await fs.writeFile(filePath, content, { encoding: "utf8", mode: 0o600 }) await fs.chmod(filePath, 0o600) } export async function writeJson(filePath: string, data: unknown): Promise { const content = JSON.stringify(data, null, 2) await writeText(filePath, content + "\n") } /** Write JSON with restrictive permissions (0o600) for files containing secrets */ export async function writeJsonSecure(filePath: string, data: unknown): Promise { const content = JSON.stringify(data, null, 2) await ensureDir(path.dirname(filePath)) await fs.writeFile(filePath, content + "\n", { encoding: "utf8", mode: 0o600 }) await fs.chmod(filePath, 0o600) } export async function walkFiles(root: string): Promise { const entries = await fs.readdir(root, { withFileTypes: true }) const results: string[] = [] for (const entry of entries) { const fullPath = path.join(root, entry.name) if (entry.isDirectory()) { const nested = await walkFiles(fullPath) results.push(...nested) } else if (entry.isFile()) { results.push(fullPath) } } return results } /** * Resolve a colon-separated command name into a filesystem path. * e.g. resolveCommandPath("/commands", "ce:plan", ".md") -> "/commands/ce/plan.md" * Creates intermediate directories as needed. */ export async function resolveCommandPath(dir: string, name: string, ext: string): Promise { const parts = name.split(":") if (parts.length > 1) { const nestedDir = path.join(dir, ...parts.slice(0, -1)) await ensureDir(nestedDir) return path.join(nestedDir, `${parts[parts.length - 1]}${ext}`) } return path.join(dir, `${name}${ext}`) } export async function copyDir(sourceDir: string, targetDir: string): Promise { await ensureDir(targetDir) const entries = await fs.readdir(sourceDir, { withFileTypes: true }) for (const entry of entries) { const sourcePath = path.join(sourceDir, entry.name) const targetPath = path.join(targetDir, entry.name) if (entry.isDirectory()) { await copyDir(sourcePath, targetPath) } else if (entry.isFile()) { await ensureDir(path.dirname(targetPath)) await fs.copyFile(sourcePath, targetPath) } } }