UNPKG

@struktoai/mirage-core

Version:

A Unified Virtual Filesystem For AI Agents

133 lines • 5.8 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 { IOResult } from "../../../io/types.js"; import { byteView } from "../../../shell/bytes.js"; import { Follow, LineFacts } from "../../../types.js"; import { respellOne } from "../../../utils/path.js"; import { rstripSlash } from "../../../utils/slash.js"; import { specOf } from "../../spec/builtins.js"; import { FlagView } from "../../spec/flag_view.js"; import { linksOf, mountView } from "../utils/links.js"; import { hostedStream, StreamHost } from "../utils/stream_host.js"; import { OTHER_FILESYSTEM, scanOperand } from "./archive/walk.js"; // What Info-ZIP calls a path it could not reach. It does not distinguish // absent from unreadable, and a dangling symlink under the default follow // prints exactly this too. const NOT_MATCHED = 'name not matched: '; function parseFlags(bag) { const fl = new FlagView(bag, specOf('zip')); return { recursive: fl.asBool('r'), junkPaths: fl.asBool('j'), quiet: fl.asBool('q'), storeLinks: fl.asBool('y'), exclude: fl.asList('x'), }; } /** * Walk the operands and hand what they hold to mirage-rs's zip, which names * each member, refuses a name two paths share, deflates them and writes the * archive. Info-ZIP's defaults are tar's inverted twice over: a directory * operand contributes only itself unless `-r` says to descend, and a symlink * is followed unless `-y` says to store the link; both are parameters of the * shared scan. Mirrors zip_cmd.py. */ export async function zipGeneric(paths, ctx, // What zip needs from the mount it runs on: a reader for the archive and // each member, and the stat and walk that make a directory archivable. deps) { const flags = parseFlags(ctx.flags); const archive = paths[0]; if (archive === undefined) { const stderr = new TextEncoder().encode('zip: usage: zip archive.zip file1 [file2 ...]\n'); return [null, new IOResult({ exitCode: 1, stderr })]; } const reads = [archive]; const found = []; const warnings = []; for (const path of paths.slice(1)) { const raw = path.rawPath; if (path.walkError !== null) { // The walk refused the operand before zip ran (the empty name, a link // loop above the name), which Info-ZIP matches to nothing. warnings.push(byteView(NOT_MATCHED + raw)); continue; } const base = rstripSlash(path.virtual) || '/'; const scan = await scanOperand(path, { stat: deps.stat, walk: deps.walk, links: linksOf(ctx) ?? null, // Every mount nested under an operand bounds the scan, a relayed run's too. mounts: mountView(ctx.vfs) ?? null, dereference: !flags.storeLinks, recurse: flags.recursive, }); for (const problem of scan.problems) { // Info-ZIP stores a directory it could not open and says nothing about // it (pinned on debian:stable-slim). if (problem.unreadable === true) continue; const shown = respellOne(problem.path, base, raw); const text = problem.fatal === true ? NOT_MATCHED + shown : `${shown}: ${problem.reason ?? ''}`; warnings.push(byteView(text)); } if (scan.missing) continue; for (const crossing of scan.crossings) { warnings.push(byteView(`${respellOne(crossing, base, raw)}: ${OTHER_FILESYSTEM}`)); } // Info-ZIP walks a bare `.` with an empty prefix, so what it finds there // is named bare. const spelling = raw === '.' ? '' : raw; for (const entry of scan.entries) { const read = entry.read ?? null; // Info-ZIP never stores the archive it is writing. if (read !== null && read.virtual === archive.virtual) continue; const item = { spelled: byteView(respellOne(entry.namePath, base, spelling)), kind: entry.kind, target: byteView(entry.target ?? ''), }; if (read !== null) { reads.push(read); item.path = byteView(read.rawPath); item.operand = reads.length; } found.push(item); } } const config = { archive: byteView(archive.rawPath), output: byteView(archive.virtual), found, warnings, exclude: flags.exclude.map((pattern) => byteView(pattern)), junk: flags.junkPaths, quiet: flags.quiet, }; const host = new StreamHost(reads, deps.stream, ctx, false); const io = new IOResult(); return [hostedStream('zip', config, host, io, ctx.signal), io]; } /** * A zip line stores a link operand as given, its planner deciding whether to * follow it (`-y`), and reads below a directory under `-r`. Mirrors Python's * `facts`. */ export function facts(fl, _operands) { return new LineFacts({ follows: Follow.NONE, readsSubtree: fl.asBool('r') }); } //# sourceMappingURL=zip_cmd.js.map