UNPKG

@struktoai/mirage-core

Version:

A Unified Virtual Filesystem For AI Agents

172 lines • 8.18 kB
// ========= Copyright 2026 @ Strukto.AI All Rights Reserved. ========= // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // ========= Copyright 2026 @ Strukto.AI All Rights Reserved. ========= import { FileType, Follow, LineFacts } from "../../../../types.js"; import { walk } from "../../generic/cp.js"; import { removeTree, rmWithoutOperands } from "../../generic/rm_cmd.js"; import { formatOptionalRecords } from "../../utils/output.js"; import { mountPoints, operandsIo } from "../../utils/operands.js"; import { removalLines } from "../../utils/verbose.js"; import { specOf } from "../../../spec/builtins.js"; import { FlagView } from "../../../spec/flag_view.js"; import { isSlashedLink, rmLinkRefusal } from "../../utils/slash_links.js"; import { innerSuffix, isEnoent, isEnotdir, isFsError, strerrorOf, withInner, } from "../../../../errors/fs.js"; import { operandSpelling } from "../../../../errors/render.js"; import { posixPhrase } from "../../../../errors/posix.js"; import { commandFiles } from "../files.js"; import { dispatchOf, linksOf, mountsOf } from "../../utils/links.js"; import { beforeFullWalk } from "../../utils/tree.js"; const rm = async (_accessor, paths, _texts, ctx) => { const files = commandFiles(ctx, paths); const fl = new FlagView(ctx.flags, specOf('rm')); const recursive = fl.asBool('r') || fl.asBool('R'); const dirFlag = fl.asBool('d'); const force = fl.asBool('f'); const verbose = fl.asBool('v'); if (paths.length === 0) return rmWithoutOperands(force); const resolved = await files.resolveGlob(paths); const lines = []; const errors = []; const links = linksOf(ctx) ?? null; for (const p of resolved) { if (isSlashedLink(p, links)) { const refusal = await rmLinkRefusal(p, links, { recursive, force }); if (refusal !== null) errors.push(refusal); continue; } let isDir = false; try { const st = await files.stat(p); isDir = st.type === FileType.DIRECTORY; } catch (err) { if (!isFsError(err)) throw err; // ENOTDIR is a component that is a plain file: the operand sits // under one, or carried a trailing slash that named one (`rm reg/`). // -f ignores it and ENOENT alone, as GNU rm 9.7 does; any other // failure is reported, -f or not. GNU rm reports the // operand and keeps removing the rest. if (force && (isEnoent(err) || isEnotdir(err))) continue; errors.push(`rm: cannot remove '${p.rawPath}': ${strerrorOf(err)}`); continue; } let entryLines = []; try { if (isDir) { // rmR/rmdir are resolved lazily so object stores without a real // directory-remove op still unlink plain files (mirrors Python). if (recursive) { // The mount is asked before anything goes, as for cp -r: a removal // cut short would leave a tree that looks untouched. await beforeFullWalk(dispatchOf(ctx), p, 'rm'); const listing = (dir) => files.readdir(dir); const probe = (spec) => files.stat(spec); // -v names each entry before the tree goes in one call; a tree it // cannot list whole goes entry by entry, as does one on a mount // without rm_r, one the dispatcher declines for the caller's view, // and one holding a mount, whose covered keys one call would take // with it. const mounts = mountsOf(ctx); const nested = mountPoints(mounts, p.virtual); const unlisted = []; let listed = verbose ? (await walk(listing, probe, p, 'rm', unlisted)).map(({ path, kind }) => ({ path, isDir: kind === FileType.DIRECTORY, })) : []; let declined = unlisted.length > 0 || (mounts?.descendants(p.virtual).length ?? 0) > 0 || !files.answers('rm_r'); if (!declined) { try { await files.rmR(p); } catch (err) { if (err.declined !== true) throw err; declined = true; } } let failures = []; if (declined) { ; ({ removed: listed, failures } = await removeTree(p, { readdir: listing, stat: probe, unlink: (spec) => files.unlink(spec), rmdir: (spec) => files.rmdir(spec), mounts, force, })); for (const [entry, why] of failures) { errors.push(`rm: cannot remove '${entry.rawPath}': ${strerrorOf(why)}`); } } entryLines = verbose ? removalLines(listed, p) : []; // A removal never crosses into a mount below, so it says so as // GNU's --one-file-system does. for (const root of nested) errors.push(`rm: skipping '${operandSpelling(root, p)}', since it's on a different device`); if (failures.length > 0) { if (verbose) for (const item of entryLines) lines.push(item); continue; } } else if (dirFlag) { if ((await files.readdir(p)).length > 0) { errors.push(`rm: cannot remove '${p.rawPath}': ${posixPhrase('ENOTEMPTY')}`); continue; } await files.rmdir(p); entryLines = [`removed directory '${p.rawPath}'`]; } else { errors.push(`rm: cannot remove '${p.rawPath}': ${posixPhrase('EISDIR')}`); continue; } } else { await files.unlink(p); entryLines = [`removed '${p.rawPath}'`]; } } catch (err) { if (!isFsError(err)) throw err; // GNU rm names the entry below the operand it could not remove // (the guard blames a read-only region by its anchor), and the // operand as typed for a refusal that names no path below it (a // store's refused key), and keeps removing the rest. errors.push(`rm: cannot remove '${withInner(p.rawPath, innerSuffix(p, err))}': ${strerrorOf(err)}`); continue; } if (verbose) for (const item of entryLines) lines.push(item); } return [formatOptionalRecords(lines), operandsIo(errors.map((line) => `${line}\n`).join(''))]; }; export const BUILDER = { name: 'rm', facts: new LineFacts({ follows: Follow.NONE, subtree: true }), write: true, fn: rm, }; //# sourceMappingURL=rm.js.map