UNPKG

@struktoai/mirage-core

Version:

A Unified Virtual Filesystem For AI Agents

135 lines • 6.25 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 { canonicalize } from "../../../../commands/builtin/generic/realpath.js"; import { dispatchStat, dotRefusal } from "../../../../commands/builtin/utils/paths.js"; import { missingOperandError } from "../../../../commands/spec/usage.js"; import { fsStrerror, walkRefusal } from "../../../../errors/fs.js"; import { posixPhrase } from "../../../../errors/posix.js"; import { fsErrorLine } from "../../../../errors/render.js"; import { PolicyDenied } from "../../../../policy/index.js"; import { encodeText } from "../../../../shell/bytes.js"; import { PathSpec } from "../../../../types.js"; import { scopePath } from "../scope.js"; import { fail, parseLine, result } from "../shared.js"; import { operandAbs } from "./ln.js"; // Any filesystem answer other than a target: a refusal (session view or // policy), EINVAL (not a link), ENOENT (absent, which is what a hidden // path answers). All of them land on GNU readlink's silent exit 1, so // this matches python's `except OSError` rather than naming errnos one // at a time — a list would silently print a raw path the first time a // dispatcher answered with an errno nobody had added yet. function readlinkRefused(err) { if (err instanceof PolicyDenied) return true; return typeof err.code === 'string'; } // The last canonicalizing flag decides how much of the path must exist, in // the mode letters canonicalize takes ('' is -f). Mirrors Python's // CANONICAL_MODES. const CANONICAL_MODES = { canonicalize: '', canonicalize_existing: 'e', canonicalize_missing: 'm', }; // readlink's operands as received: every word that is not an option, the // words after `--` included. None of its options takes a value. Mirrors // Python's operand_words. export function operandWords(args) { const words = []; let options = true; for (const arg of args) { const text = scopePath(arg); if (options && text === '--') options = false; else if (!(options && text.startsWith('-') && text !== '-')) words.push(arg); } return words; } // Print a symlink's target, GNU readlink semantics. // // The three canonicalizing flags differ only in how much of the resolved // path has to exist: -m requires nothing, -f requires every component // but the last, and -e requires all of it. A path that falls short // prints nothing and exits 1, and says why under -v (the last of -q, -s // and -v wins). export async function handleReadlink(namespace, dispatch, session, args) { const [, fl, refused] = parseLine('readlink', args, session.cwd); if (refused !== null) return refused; const operands = operandWords(args); if (operands.length === 0) { const error = missingOperandError('readlink', null); return fail('readlink', `${error.message}\n`, error.exitCode); } const canon = fl .typedOrder('canonicalize', 'canonicalize_existing', 'canonicalize_missing') .at(-1); const mode = canon === undefined ? null : (CANONICAL_MODES[canon] ?? null); const verbose = fl.typedOrder('quiet', 'silent', 'verbose').at(-1) === 'verbose'; const errors = []; let newline = !fl.asBool('no_newline'); if (operands.length > 1 && !newline) { errors.push('readlink: ignoring --no-newline with multiple arguments\n'); newline = true; } const links = namespace.sessionLinks(session); const follow = (v) => links.follow(v); const readlink = (v) => links.readlink(v); const lines = []; let exitCode = 0; for (const op of operands) { const absOp = operandAbs(links, op, session.cwd); const spec = PathSpec.fromStrPath(op, undefined, session.cwd); // The link entry is namespace state behind the dispatcher: session grants // and admission policies decide whether this session may read the // target at all, so a link operand clears it even under -f, -e and -m. // EINVAL (not a link), a refusal and a failed walk all land on GNU // readlink's exit 1, said only under -v. try { if (mode !== null) { if (links.isLink(absOp)) await dispatch('readlink', PathSpec.fromStrPath(absOp)); lines.push(await canonicalize(spec.rawPath, session.cwd, mode, false, readlink, dispatchStat(dispatch))); continue; } const refusal = spec.walkError !== null ? walkRefusal(spec) : await dotRefusal(dispatchStat(dispatch), spec, follow); if (refusal !== null) throw refusal; const [found] = await dispatch('readlink', PathSpec.fromStrPath(absOp)); lines.push(found); } catch (err) { if (!readlinkRefused(err)) throw err; exitCode = 1; if (verbose) { const line = fsErrorLine('readlink', spec.rawPath, err); const code = err.code; errors.push(fsStrerror(err) === null && code === 'EINVAL' ? `${line.trimEnd()}: ${posixPhrase('EINVAL')}\n` : line); } } } const end = newline ? (fl.asBool('zero') ? '\0' : '\n') : ''; const stderr = errors.join(''); if (lines.length === 0) return result('readlink', { exitCode, stderr }); const text = lines.map((l) => l + end).join(''); return result('readlink', { out: encodeText(text), exitCode, stderr }); } //# sourceMappingURL=readlink.js.map