UNPKG

@struktoai/mirage-core

Version:

A Unified Virtual Filesystem For AI Agents

218 lines • 9.92 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 { dispatchStat, dotRefusal } from "../../../../commands/builtin/utils/paths.js"; import { invalidArgumentError, usageHint } from "../../../../commands/spec/usage.js"; import { matchWord } from "../../../../commands/spec/word_choice.js"; import { DEFAULT_UMASK } from "../../../../context/session_context.js"; import { fsStrerror, isAborting, isEnoent, isEnotdir, isMissingOp, isRefusal, strerrorOf, walkRefusal, } from "../../../../errors/fs.js"; import { IOResult } from "../../../../io/types.js"; import { decodeText } from "../../../../shell/bytes.js"; import { PathSpec } from "../../../../types.js"; import { expandOperands, fail, finish, parseLine } from "../shared.js"; import { followOperand, nowIso, parseTouchStamp, permissionError, setattrLink, setattrVia, } from "./metadata.js"; // GNU touch's --time words, aliases of one value together. Mirrors // Python's TIME_GROUPS. const TIME_GROUPS = [ ['atime', 'access', 'use'], ['mtime', 'modify'], ]; // touch: set access/modification times, creating missing files. GNU flags: // -a/-m select which times, -c no-create, -h no-dereference (writes the // link node's own mtime), -t STAMP / -d STRING supply the time, -r FILE // copies times from a reference file. export async function handleTouch(namespace, dispatch, session, args) { const [parsed, fl, refused] = parseLine('touch', args, session.cwd); if (refused !== null) return refused; let time = fl.asStr('time'); if (time !== undefined) { const match = matchWord(time, TIME_GROUPS); if (!match.matched) { const [message, code] = invalidArgumentError('touch', '--time', time, TIME_GROUPS, undefined, match.kind); return fail('touch', decodeText(message), code); } time = match.word; } const dateText = fl.asStr('date') ?? null; const ref = fl.raw('reference'); let stamp; try { stamp = parseTouchStamp(fl.asStr('t') ?? null, null); } catch (err) { const text = err instanceof Error ? err.message : String(err); return fail('touch', `touch: invalid date format '${text}'\n`, 1); } if (stamp !== null && (dateText !== null || ref instanceof PathSpec)) { return fail('touch', `touch: cannot specify times from more than one source\n${usageHint('touch')}\n`, 1); } let refTime = null; if (ref instanceof PathSpec) { try { const [refStat] = await dispatch('stat', ref); refTime = refStat.modified; } catch (err) { if (isAborting(err)) throw err; return fail('touch', `touch: failed to get attributes of '${ref.rawPath}': ${strerrorOf(err)}\n`); } } try { stamp ??= parseTouchStamp(null, dateText) ?? refTime; } catch { return fail('touch', `touch: invalid date format '${String(dateText)}'\n`, 1); } stamp ??= nowIso(); // GNU checks for a file operand only once the times are settled. const operands = parsed.paths; if (operands.length === 0) { return fail('touch', `touch: missing file operand\n${usageHint('touch')}\n`, 1); } const onlyAtime = fl.asBool('a') || time === 'atime'; const onlyMtime = fl.asBool('m') || time === 'mtime'; const setAtime = onlyAtime || !onlyMtime; const setMtime = onlyMtime || !onlyAtime; const noCreate = fl.asBool('no_create'); const errors = []; const links = namespace.sessionLinks(session); for (const target of await expandOperands(namespace, dispatch, operands)) { if (fl.asBool('no_dereference') && links.isLink(target.virtual)) { try { await setattrLink(dispatch, target, { mtime: stamp }); } catch (err) { if (!isRefusal(err)) throw err; errors.push(permissionError('touch', 'setting times of', target.rawPath, err)); } continue; } if (target.walkError !== null) { // Past -h, which acts on a looping link itself: the empty name, // whose `virtual` is the working directory, and a link loop name // nothing to touch. -c never opens the file, so it meets the walk // when it sets the times, where ENOENT is the silent miss -c asks // for. if (noCreate && target.walkError === 'ENOENT') continue; const action = noCreate ? 'setting times of' : 'cannot touch'; errors.push(`touch: ${action} '${target.rawPath}': ${String(fsStrerror(walkRefusal(target)))}\n`); continue; } if (links.isMount(target.virtual)) { errors.push(`touch: cannot touch '${target.rawPath}': Is a directory\n`); continue; } // `x/` is `x/.`, so touch never creates through a trailing slash: it // sets times on a directory that has to be there already, and GNU // words that refusal ("setting times of") differently from its // create-path one ("cannot touch"). const slashed = target.rawPath.endsWith('/'); let unwalked; try { unwalked = await dotRefusal(dispatchStat(dispatch), target, (v) => links.follow(v)); } catch (err) { if (isAborting(err)) throw err; unwalked = err; } if (unwalked !== null) { const action = slashed ? 'setting times of' : 'cannot touch'; errors.push(`touch: ${action} '${target.rawPath}': ${strerrorOf(unwalked)}\n`); continue; } const resolved = followOperand(links, 'touch', noCreate ? 'setting times of' : 'cannot touch', target, errors); if (resolved === null) continue; if (slashed) { try { await dispatch('stat', resolved); } catch (err) { if (isAborting(err)) throw err; errors.push(`touch: setting times of '${target.rawPath}': ${strerrorOf(err)}\n`); continue; } } try { try { await dispatch('stat', resolved); } catch (err) { // -c never opens the file, so GNU meets the bad parent when it sets // the times, and says so in those words. if (isEnotdir(err) && noCreate) { errors.push(`touch: setting times of '${target.rawPath}': Not a directory\n`); continue; } if (!isEnoent(err)) throw err; if (noCreate) continue; try { await dispatch('write', resolved, [new Uint8Array(0)]); } catch (werr) { // Stat-only backend (e.g. an API surface): creation is // impossible, which GNU reports as EROFS. if (!isMissingOp(werr, 'write')) throw werr; errors.push(`touch: cannot touch '${target.rawPath}': Read-only file system\n`); continue; } // A file touch creates is 0666 under the session's umask; only // a mask away from bash's default is worth a mode write, since // 644 is what a fresh file renders as. if (session.umask !== DEFAULT_UMASK) { const fields = { mode: 0o666 & ~session.umask }; if (setAtime) fields.atime = stamp; if (setMtime) fields.mtime = stamp; await setattrVia(dispatch, resolved, fields); continue; } } const fields = {}; if (setAtime) fields.atime = stamp; if (setMtime) fields.mtime = stamp; await setattrVia(dispatch, resolved, fields); } catch (err) { if (isRefusal(err)) { const action = noCreate ? 'setting times of' : 'cannot touch'; errors.push(permissionError('touch', action, target.rawPath, err)); continue; } // A destination whose parent chain is not all directories is one // failed operand, not an aborted command: GNU reports it and touches // the rest. Caught here rather than around the write because backends // disagree about which call refuses first (an object store answers // stat with ENOENT and fails the write; a filesystem or a keyed store // answers stat itself with ENOTDIR). A store's own error (an upload's // HTTP status) is one failed operand too, worded by its message. if (isAborting(err)) throw err; errors.push(`touch: cannot touch '${target.rawPath}': ${strerrorOf(err)}\n`); } } return finish('touch', errors, new IOResult()); } //# sourceMappingURL=touch.js.map