@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
245 lines • 11.2 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 { formatOptionalRecords } from "../../utils/output.js";
import { operandsIo } from "../../utils/operands.js";
import { posixPhrase } from "../../../../errors/posix.js";
import { errorVirtualPath, fsStrerror, isFsError } from "../../../../errors/fs.js";
import { operandSpelling } from "../../../../errors/render.js";
import { DEFAULT_DIR_MODE, parseChmod } from "../../../../utils/mode.js";
import { DEFAULT_UMASK, sessionUmask, walkProbeFor } from "../../../../context/session_context.js";
import { specOf } from "../../../spec/builtins.js";
import { FlagView } from "../../../spec/flag_view.js";
import { mkdirLinkRefusal } from "../../utils/slash_links.js";
import { descendantPath, dispatchStat, entryKind, linkFollow, nearestAncestor, } from "../../utils/paths.js";
import { FileType, Follow, LineFacts, PathSpec } from "../../../../types.js";
import { CycleError, norm, parent, walkNodes } from "../../../../utils/path.js";
import { commandFiles } from "../files.js";
import { missingOperandError } from "../../../spec/usage.js";
import { linksOf, mountRoot } from "../../utils/links.js";
/**
* Make one name of a walk a directory, or say why it is not one.
*
* Judged the way GNU's walk into the name is, before anything is made: a
* directory, or a link to one, is passed through; a plain file, or a link to
* one, is ENOTDIR; a dangling link is EEXIST, and a looping one ELOOP. Only
* a missing name is made,
* where its links lead, so no store is asked to make a directory over a file
* or under a link it cannot see. Outside a workspace command there is no stat
* to judge with, and the store's own mkdir answers. Mirrors Python's
* _enter_node.
*/
async function enterNode(mkdir, path, node, root, links, probe) {
if (links !== null && links.statAt(node) !== null) {
try {
links.resolve(node);
}
catch (err) {
if (!(err instanceof CycleError))
throw err;
return posixPhrase('ELOOP');
}
const target = await links.targetStat(node);
if (target === null)
return posixPhrase('EEXIST');
return target.type === FileType.DIRECTORY ? null : posixPhrase('ENOTDIR');
}
if (probe !== null) {
const { exists, isDir } = await entryKind(probe.stat, PathSpec.fromStrPath(node));
if (exists)
return isDir ? null : posixPhrase('ENOTDIR');
}
const real = links !== null ? links.resolve(node) : node;
if (real.startsWith(`${root}/`))
await mkdir(descendantPath(path, real), true);
return null;
}
/**
* Make every name an operand's walk enters, GNU `mkdir -p` style.
*
* The backend's `parents` makes the ancestors of the simplified path, which
* skips a name the walk passes through on its way to a `..`: GNU creates
* `nope` for `mkdir -p nope/../m`. The operand itself is left to the caller;
* a name in the way is quoted as the operand spells it. Null when every name
* is made. Mirrors Python's _make_walked.
*/
async function makeWalked(mkdir, path, dotted, root, links, probe) {
const follow = links === null ? null : (virtual) => links.resolve(virtual);
for (const [node, spelled] of walkNodes(dotted, path.rawPath, follow)) {
let why;
try {
why = await enterNode(mkdir, path, node, root, links, probe);
}
catch (err) {
if (!isFsError(err))
throw err;
why =
err.code === 'EEXIST'
? posixPhrase('ENOTDIR')
: String(fsStrerror(err));
}
if (why !== null)
return `mkdir: cannot create directory '${spelled}': ${why}`;
}
return null;
}
/**
* Make one mkdir operand, or the line GNU reports when it cannot.
*
* One unusable operand is not an aborted command: GNU reports it and still
* makes the remaining directories. The error names the path to quote:
* usually the operand, but `mkdir -p` blames the component of the chain it
* tripped on. Every mkdir makes its operands here, a keyed store's override
* included, so they report alike. Mirrors Python's make_directory.
*/
// The names a verbose mkdir reports for `path`, top-down, as GNU spells them.
// One backend mkdir makes a `-p` chain without saying which names it made, so
// the chain is probed before the create: every name below the nearest
// existing ancestor, or none when `path` already exists. A dotted operand is
// walked as typed, the way `-p` enters it, so `nope/../m` reports `nope` too,
// each name spelled by the prefix of the operand that reaches it. Outside a
// workspace there is nothing to probe with, and the operand alone is
// reported. Mirrors Python's created_names.
export async function createdNames(path, parents, links = null, probe = walkProbeFor(path.virtual)) {
if (!parents || probe === null)
return [operandSpelling(path.virtual, path)];
const named = PathSpec.fromStrPath(path.virtual);
const names = [];
if (path.dotted !== null) {
const follow = links === null ? null : (virtual) => links.resolve(virtual);
const made = new Set();
for (const [node, spelled] of walkNodes(path.dotted, path.rawPath, follow)) {
if (made.has(node) || (await entryKind(probe.stat, PathSpec.fromStrPath(node))).exists) {
continue;
}
made.add(node);
names.push(spelled);
}
if (made.has(norm(path.virtual)) || (await entryKind(probe.stat, named)).exists)
return names;
return [...names, operandSpelling(path.virtual, path)];
}
if ((await entryKind(probe.stat, named)).exists)
return [];
const [top] = await nearestAncestor(probe.stat, named);
let node = norm(path.virtual);
while (node !== top && node !== '/') {
names.push(operandSpelling(node, path));
node = parent(node);
}
return names.reverse();
}
// GNU's `mkdir -v` lines. Mirrors Python's created_lines.
export function createdLines(names) {
return names.map((name) => `mkdir: created directory '${name}'`);
}
export async function makeDirectory(mkdir, path, parents, root, links = null, probe = walkProbeFor(path.virtual)) {
let target = path;
// -p enters the names in front of the operand one at a time, so a dot
// among them, or a link loop the walk refused the operand for, is met at
// that name and GNU quotes it rather than the operand.
if (parents && (path.dotted !== null || path.walkError === 'ELOOP')) {
const failed = await makeWalked(mkdir, path, path.dotted ?? path.virtual, root, links, probe);
if (failed !== null)
return failed;
// The walk has entered every name the spelling passes through, so the
// operand is made by its resolved path alone: walking it again would ask
// a store that shows no empty directory (hf) for one the walk just made.
target = new PathSpec({
virtual: path.virtual,
directory: path.directory,
vfsPath: path.vfsPath,
pattern: path.pattern,
resolved: path.resolved,
rawPath: path.rawPath,
walkError: path.walkError,
});
}
try {
await mkdir(target, parents);
}
catch (err) {
if (!isFsError(err))
throw err;
const named = operandSpelling(errorVirtualPath(err), path);
return `mkdir: cannot create directory '${named}': ${String(fsStrerror(err))}`;
}
return null;
}
const mkdir = async (_accessor, paths, _texts, ctx) => {
const files = commandFiles(ctx, paths);
const fl = new FlagView(ctx.flags, specOf('mkdir'));
const parents = fl.asBool('parents');
const verbose = fl.asBool('verbose');
const modeText = fl.asStr('mode') ?? null;
if (paths.length === 0)
throw missingOperandError('mkdir', null);
let mode = null;
if (modeText !== null) {
// Symbolic clauses build on what mirage renders for a new
// directory; `-m` is applied after the create, so the session's
// umask does not reach it, which is GNU's rule too.
mode = parseChmod(modeText, DEFAULT_DIR_MODE);
if (mode === null)
throw new Error(`mkdir: invalid mode '${modeText}'`);
}
else {
// A new directory is 0777 masked by the session's umask. Only a
// mask away from bash's default costs a setattr, since 755 is what
// every backend already renders for a fresh directory; parents
// made by `-p` keep that default. A backend without permission
// bits keeps the mode in the attribute overlay, as it keeps a chmod.
const umask = sessionUmask(ctx.session?.access());
if (umask !== DEFAULT_UMASK)
mode = 0o777 & ~umask;
}
const resolved = await files.resolveGlob(paths);
const lines = [];
const errors = [];
const links = linksOf(ctx) ?? null;
const probe = ctx.vfs === undefined
? null
: { stat: dispatchStat(ctx.vfs.dispatch), follow: linkFollow(links) };
for (const p of resolved) {
const collision = await mkdirLinkRefusal(p, links, { parents });
if (collision.taken) {
if (collision.message !== null)
errors.push(collision.message);
continue;
}
const names = verbose ? await createdNames(p, parents, links, probe) : [];
const existed = mode !== null && (await entryKind(files.stat, p)).exists;
const failed = await makeDirectory(files.mkdir, p, parents, mountRoot(ctx), links, probe);
if (failed !== null) {
errors.push(failed);
continue;
}
// -m applies to the named directory only, and only when this mkdir made
// it: parents made by -p keep the default mode, a directory -p found
// keeps its own (GNU), and a store that keeps no empty directory (hf)
// has none to set it on.
if (mode !== null && !existed && (await entryKind(files.stat, p)).exists) {
await files.setAttrs(p, { mode });
}
for (const item of createdLines(names))
lines.push(item);
}
return [formatOptionalRecords(lines), operandsIo(errors.map((line) => `${line}\n`).join(''))];
};
export const BUILDER = {
name: 'mkdir',
facts: new LineFacts({ follows: Follow.NONE }),
write: true,
fn: mkdir,
};
//# sourceMappingURL=mkdir.js.map