@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
158 lines • 6.96 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 { FALLBACK_TMPDIR } from "../constants.js";
import { specOf } from "../../spec/builtins.js";
import { FlagView } from "../../spec/flag_view.js";
import { UsageError } from "../../errors.js";
import { quoteText } from "../../quote.js";
import { IOResult } from "../../../io/types.js";
import { LineFacts, PathSpec } from "../../../types.js";
import { eexist, fsStrerror, isEnoent, isFsError } from "../../../errors/fs.js";
import { extraOperandError } from "../../spec/usage.js";
import { CommandName } from "../../spec/types.js";
import { SystemIdSource, randomBelow } from "../../../utils/ids.js";
const ENC = new TextEncoder();
const ALPHABET = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
const DEFAULT_TEMPLATE = 'tmp.XXXXXXXXXX';
// How many names a create draws before it gives up with EEXIST.
const ATTEMPTS = 100;
function randomSuffix(ids, length) {
let out = '';
for (let i = 0; i < length; i++) {
out += ALPHABET[randomBelow(ids, ALPHABET.length)] ?? '';
}
return out;
}
/**
* The template a create names, formed as GNU mktemp forms it: the formed
* template, how many X's end its body, and the length of the suffix after
* them. The name stays as typed, so a relative template or directory prints
* and refuses relative: a bare template lives in the working directory, and
* only a line with no template, -p/--tmpdir or -t joins a directory in
* front of it ($TMPDIR if set, else /tmp; -t prefers $TMPDIR over -p).
* Pinned against GNU coreutils 9.7 (debian:stable-slim). Mirrors Python's
* plan_template.
*/
export function planTemplate(templateArg, suffixArg, destDir, useDestDir, t, envTmpdir) {
let template = templateArg ?? DEFAULT_TEMPLATE;
let suffix;
if (suffixArg !== undefined) {
if (!template.endsWith('X')) {
throw new UsageError(`mktemp: with --suffix, template '${quoteText(template)}' must end in X`, 1);
}
suffix = suffixArg;
template += suffix;
}
else {
const lastX = template.lastIndexOf('X');
suffix = lastX >= 0 ? template.slice(lastX + 1) : '';
}
if (suffix.includes('/')) {
throw new UsageError(`mktemp: invalid suffix '${quoteText(suffix)}', contains directory separator`, 1);
}
const body = template.slice(0, template.length - suffix.length);
const xCount = body.length - body.replace(/X+$/, '').length;
if (xCount < 3) {
throw new UsageError(`mktemp: too few X's in template '${quoteText(template)}'`, 1);
}
if (useDestDir || t || templateArg === undefined) {
let directory;
if (t) {
directory = envTmpdir || destDir || FALLBACK_TMPDIR;
if (template.includes('/')) {
throw new UsageError(`mktemp: invalid template, '${quoteText(template)}', contains directory separator`, 1);
}
}
else {
directory = destDir || envTmpdir || FALLBACK_TMPDIR;
if (template.startsWith('/')) {
throw new UsageError(`mktemp: invalid template, '${quoteText(template)}'; with --tmpdir, it may not be absolute`, 1);
}
}
template = `${directory}${directory.endsWith('/') ? '' : '/'}${template}`;
}
return [template, xCount, suffix.length];
}
function parseFlags(bag) {
const fl = new FlagView(bag, specOf('mktemp'));
return {
directory: fl.asBool('directory'),
tmpdir: fl.asPaths('tmpdir')[0] ?? fl.asPaths('p')[0] ?? null,
useDestDir: fl.raw('tmpdir') !== undefined || fl.raw('p') !== undefined,
templateMode: fl.asBool('t'),
dryRun: fl.asBool('dry_run'),
suffix: fl.asStr('suffix') ?? null,
quiet: fl.asBool('quiet'),
};
}
export async function mktempGeneric(texts, ctx, mkdir, write, exists) {
const parsed = parseFlags(ctx.flags);
if (texts.length > 1)
throw extraOperandError(CommandName.MKTEMP, texts[1] ?? '');
const { directory, tmpdir, useDestDir, templateMode: t } = parsed;
const destDir = tmpdir?.rawPath ?? '';
const envTmpdir = ctx.env?.TMPDIR ?? '';
const [template, xCount, suffixLen] = planTemplate(texts[0], parsed.suffix ?? undefined, destDir, useDestDir, t, envTmpdir);
const fallback = (texts.length === 0 || useDestDir || t) && destDir === '' && envTmpdir === '';
const end = template.length - suffixLen;
const ids = ctx.ids ?? new SystemIdSource();
const draw = () => template.slice(0, end - xCount) + randomSuffix(ids, xCount) + template.slice(end);
const create = (path) => directory ? mkdir(path) : write(path, new Uint8Array(0));
let name = draw();
try {
let path = PathSpec.fromStrPath(name, undefined, ctx.cwd);
let attempt = 0;
while (exists !== undefined && (await exists(path))) {
attempt += 1;
if (attempt >= ATTEMPTS)
throw eexist(path.virtual);
name = draw();
path = PathSpec.fromStrPath(name, undefined, ctx.cwd);
}
if (!parsed.dryRun) {
try {
await create(path);
}
catch (error) {
if (!fallback || !isEnoent(error))
throw error;
await mkdir(PathSpec.fromStrPath(FALLBACK_TMPDIR));
await create(path);
}
}
}
catch (error) {
if (!isFsError(error))
throw error;
// -q suppresses the diagnostic about the create only (GNU); a bad
// template still says so.
if (parsed.quiet)
return [null, new IOResult({ exitCode: 1 })];
const kind = directory ? 'directory' : 'file';
return [
null,
new IOResult({
exitCode: 1,
stderr: ENC.encode(`mktemp: failed to create ${kind} via template '${quoteText(template)}': ${String(fsStrerror(error))}\n`),
}),
];
}
const result = ENC.encode(name + '\n');
return [result, new IOResult()];
}
/** A mktemp line creates its file unless `-u`. Mirrors Python's `facts`. */
export function facts(fl, _operands) {
return new LineFacts({ writes: !fl.asBool('dry_run') });
}
//# sourceMappingURL=mktemp.js.map