This commit is contained in:
2026-08-17 10:43:38 +02:00
parent e126182ae6
commit 3a941480a9
20 changed files with 430 additions and 46 deletions

View File

@@ -1,4 +1,4 @@
import { mkdir, readdir, readFile, rm, stat, writeFile } from 'node:fs/promises'
import { mkdir, readdir, readFile, rename, rm, stat, writeFile } from 'node:fs/promises'
import { dirname, join, relative, sep } from 'node:path'
import { listFiles, rgAvailable } from './search-service'
@@ -261,6 +261,29 @@ export async function createProjectDir(root: string, rel: string): Promise<void>
await mkdir(target, { recursive: true })
}
/**
* Rename a project file or folder. `rel` is the current relative path, `name`
* the new basename (no slashes — a rename stays in the same folder). Returns the
* new relative path. Refuses to escape the project root and throws if the target
* name is already taken, so a rename never clobbers an existing file.
*/
export async function renameProjectEntry(root: string, rel: string, name: string): Promise<string> {
const clean = name.trim().replace(/\/+$/, '')
if (!clean || clean.includes('/') || clean === '.' || clean === '..') throw new Error('invalid name')
const from = join(root, rel)
const parent = rel.includes('/') ? rel.slice(0, rel.lastIndexOf('/')) : ''
const next = parent ? `${parent}/${clean}` : clean
const to = join(root, next)
if (relative(root, from).startsWith('..') || relative(root, to).startsWith('..')) throw new Error('outside project root')
if (from === to) return rel
// Case-only renames (foo.md → Foo.md) hit an existing path on macOS' case
// insensitive filesystem, so only guard when the name really differs.
const sameName = from.toLowerCase() === to.toLowerCase()
if (!sameName && await stat(to).catch(() => null)) throw new Error('name already exists')
await rename(from, to)
return next
}
/** Delete a project file or folder (relative path). Stays inside the project root. */
export async function deleteProjectFile(root: string, rel: string): Promise<void> {
const target = join(root, rel)