@notrealstudio/backend-fs
v0.1.1
Published
Файлы пространства на диске для nr-ui-protocol: createFsWorkspace(root) — list/read/write/delete/rename/stat/watch (capability workspace.files)
Readme
@notrealstudio/backend-fs
Файлы рабочего пространства на диске для nr-ui-protocol (workspace-files-spec §1–2):
createFsWorkspace(root) → WorkspaceFilesOps — list / read / write /
delete / rename / stat / watch.
Не бэкенд, а его файловая часть. backend-pi отдаёт через неё cwd агента,
backend-local (exz v2) — каталог проекта. Зависимости — node builtins и
@notrealstudio/nr-ui-protocol.
import { createFsWorkspace } from '@notrealstudio/backend-fs'
const files = createFsWorkspace(process.cwd(), { write: true, watch: true })
caps.workspace = { files: files.capability } // { write: true, watch: true }
backend.workspace = { current, list, files }
const off = files.watch!((ev) => console.log(ev.paths)) // workspace_update
await files.write!('notes/todo.md', '- [ ] …', { revision: null })
off()
files.close()| opt | default | что |
|---|---|---|
| write | true | write / delete / rename; false — методов нет, capability без write |
| watch | true | watch через fs.watch рекурсивно |
| debounceMs | 200 | окно склейки изменений в одно событие |
| maxWatchPaths | 500 | больше путей в окне — paths: [''] |
| warn | stderr | варнинги watch |
Метод есть ⇔ флаг capability заявлен — files.capability можно отдавать в
Capabilities.workspace.files как есть.
Пути
Относительные от корня, POSIX-слэши, корень — ''. Нормализация и отказы —
normalizeFilePath протокола: .., абсолютные пути, \, NUL → bad_request.
Сверху — то, что знает только диск: симлинк, раскрытый за пределы корня,
— bad_request, в том числе битый (запись сквозь битую ссылку ушла бы
наружу). Ссылки внутри пространства работают; в list ссылок наружу нет.
Корень и корзину (.nr-trash) файловые операции не меняют — bad_request.
Операции
list(path?)— один уровень: каталоги первыми, дальше по имени.sizeиmimeу файлов,mtimeу всех.ignored—.gitиnode_modulesна любой глубине,.nr-trashв корне,.gitignoreкаталогов (подмножество git:!, якоря,dir/,*,?,**,[…]). Сокеты и FIFO не показываются.read(path, {maxBytes})— UTF-8 без NUL текстом (BOM сохраняется), иначе base64. Размер больше лимита (default 2 МБ) —bad_requestс размером, до чтения байтов.revision=size:mtimeMsтого дескриптора, что читали.write(path, content, {encoding, revision})— атомарно: временный.<имя>.nr-tmp-*рядом +rename, права существующего файла сохраняются, родительские каталоги создаются.revisionразошёлся или файла больше нет —conflict;revision: nullповерх существующего —conflict; безrevision— запись как есть. Возвращает новыйrevision. Мутации одного пространства идут по очереди — проверка ревизии и запись не перемежаются в процессе.delete(path)— не стирает: переносит в<root>/.nr-trash/<путь>; место занято — с суффиксом времени. Ссылка удаляется сама, не её цель.rename(from, to)—toзанят —conflict; внутрь себя —bad_request; смена регистра имени на регистронезависимой ФС работает.stat(path)—FileEntry; у корняpath: '',name— имя каталога.
Нет файла — not_found. Коды — FileBadRequestError / FileNotFoundError /
FileConflictError протокола: транспорт (serveBackend) отдаёт их кодом.
watch
fs.watch(root, {recursive: true}) запускается на первого подписчика и
снимается с последним. События за окно debounceMs склеиваются в один
workspace_update {paths} — правки агента (pi edit/write) приезжают так же,
как свои.
- временные файлы атомарной записи и внутренности
.git— не события; - всё внутри игнорируемого каталога сворачивается в сам каталог
(
node_modules/x/y.js→node_modules); - больше
maxWatchPathsпутей или шторм —paths: [''], «перечитай всё»; - watch упал (корень удалили) — последнее событие
[''], дальше тишина.
⚠ Корзина в git
.nr-trash/ создаётся в корне пространства при первом delete. В .gitignore
проекта пакет её не дописывает — чужой файл не правим. Добавьте строку
.nr-trash/ сами, если пространство — git-репозиторий.
Разработка
npm test # vitest: временные каталоги, настоящий fs.watch
npm run build # tsc → dist