UNPKG

@struktoai/mirage-core

Version:

A Unified Virtual Filesystem For AI Agents

158 lines • 6.96 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 { 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