@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
133 lines • 5.8 kB
JavaScript
// ========= 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