UNPKG

@struktoai/mirage-core

Version:

A Unified Virtual Filesystem For AI Agents

255 lines • 9.94 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 git, { Errors } from 'isomorphic-git'; import { IOResult } from "../../../../io/types.js"; import { readStdinAsync } from "../../../builtin/utils/stream.js"; import { FlagView } from "../../../spec/flag_view.js"; import { AmbiguousArgumentError, GitError, IncompatibleOptionsError, InvalidRevisionNameError, UsageError, } from "./errors.js"; import { OBJECT_TYPES } from "./constants.js"; import { rawObject, repoArgs } from "./repo.js"; import { resolveObject } from "./revparse.js"; import { guarded, located, placeNamed, rooted, shownItems, shownRaw } from "./seen.js"; import { opened } from "./session.js"; import { checkSwitches, verbUsage } from "./util.js"; const ENC = new TextEncoder(); const DEC = new TextDecoder(); // The query modes, each named by the switch that selects it. const QUERIES = ['t', 's', 'e', 'p']; const BATCH_FORMAT = '%(objectname) %(objecttype) %(objectsize)'; const FORMAT_ATOM = /%\(([^)]*)\)/g; const BATCH_ATOMS = new Set(['objectname', 'objecttype', 'objectsize', 'rest']); const HEX_ID = /^[0-9a-f]{40}$/; /** The object a name stands for, in cat-file's words when it stands for none. */ async function namedObject(repo, name) { try { return await resolveObject(repo, name); } catch (err) { if (err instanceof AmbiguousArgumentError || err instanceof InvalidRevisionNameError) { throw new GitError(`Not a valid object name ${name}`); } throw err; } } /** An object's bytes as stored. */ async function raw(repo, oid) { return (await rawObject(repo, oid)).raw; } /** * An object peeled down to a type, as `git cat-file <type>` peels it: a tag to * what it points at and a commit to its tree; null when the chain never * reaches the type. */ async function peeled(repo, obj, want) { let at = obj; while (at.type !== want) { if (at.type === 'tag') { const { tag } = await git.readTag({ ...repoArgs(repo), oid: at.oid }); at = { oid: tag.object, type: tag.type }; // A tag can point past every commit at a tree or a blob, which is there // only where the session is shown it. if ((await located(repo, at)) === null) return null; } else if (at.type === 'commit' && want === 'tree') { const { commit } = await git.readCommit({ ...repoArgs(repo), oid: at.oid }); rooted(repo, commit.tree); at = { oid: commit.tree, type: 'tree' }; } else { return null; } } return at; } /** * The stored bytes of what a name stands for, peeled to a type, as the session * is shown them; null when the chain never reaches the type. * * @param repo the opened repository * @param name the object name as typed * @param want the type asked for */ export async function typedBytes(repo, name, want) { const obj = await peeled(repo, await namedObject(repo, name), want); if (obj === null) return null; return shownRaw(repo, obj, name); } /** * `-p`: a tree as ls-tree lists it, the entries the session sees at the place * the name gave it, any other object as stored. * * @param repo the opened repository * @param obj the object * @param name the object name as typed */ async function pretty(repo, obj, name) { if (obj.type !== 'tree') return raw(repo, obj.oid); return ENC.encode((await shownItems(repo, obj.oid, await placeNamed(repo, name, obj.oid))) .map((entry) => `${entry.mode.padStart(6, '0')} ${entry.type} ${entry.oid}\t${entry.path}\n`) .join('')); } /** One query mode's answer for one name, and its exit status. */ async function query(repo, mode, name) { if (mode === 'e' && HEX_ID.test(name)) { if (!guarded(repo)) { try { await raw(repo, name); return [new Uint8Array(), 0]; } catch (err) { if (err instanceof Errors.NotFoundError) return [new Uint8Array(), 1]; throw err; } } // The session could be hiding what the id names, so only an object it is // shown exists; any other is as silent as none. try { await namedObject(repo, name); } catch (err) { if (err instanceof GitError) return [new Uint8Array(), 1]; throw err; } return [new Uint8Array(), 0]; } const obj = await namedObject(repo, name); if (mode === 't') return [ENC.encode(`${obj.type}\n`), 0]; if (mode === 's') { const shown = await shownRaw(repo, obj, name); return [ENC.encode(`${String(shown.length)}\n`), 0]; } if (mode === 'e') return [new Uint8Array(), 0]; return [await pretty(repo, obj, name), 0]; } /** * One `--batch`/`--batch-check` format line, the object it names and the name; * `<name> missing` and null for a name that stands for nothing. With `%(rest)` * in the format the name ends at the first blank and the rest of the line is * `%(rest)`. */ async function batchHead(repo, line, template) { let name = line; let rest = ''; if (template.includes('%(rest)')) { const blank = line.indexOf(' '); if (blank !== -1) { name = line.slice(0, blank); rest = line.slice(blank + 1); } } let obj; try { obj = await resolveObject(repo, name); } catch (err) { if (err instanceof GitError) return [ENC.encode(`${name} missing\n`), null, name]; throw err; } const atoms = { objectname: obj.oid, objecttype: obj.type, objectsize: String((await shownRaw(repo, obj, name)).length), rest, }; const head = template.replace(FORMAT_ATOM, (_, atom) => atoms[atom] ?? ''); return [ENC.encode(`${head}\n`), obj, name]; } /** * The batch answers in order, each object's bytes read only when its turn * comes, so a long `--batch` never holds every object at once; a record goes out * whole or not at all. */ async function* batchLines(repo, heads, contents) { for (const [head, obj, name] of heads) { const body = contents && obj !== null ? await shownRaw(repo, obj, name) : null; yield head; if (body !== null) { yield body; yield ENC.encode('\n'); } } } /** * `git cat-file`: an object's type, size, existence or content, for one name, a * `<type> <object>` pair or each stdin line under `--batch`. Pinned against git * 2.50.1. */ export async function catFile(_accessor, paths, texts, ctx) { const fl = new FlagView(ctx.flags); checkSwitches(ctx, texts); const modes = fl.typedOrder(...QUERIES); const [first, second] = modes; if (first !== undefined && second !== undefined) { throw new IncompatibleOptionsError(`-${second}`, `-${first}`); } const batch = fl.typedOrder('batch', 'batch_check').at(-1); if (batch !== undefined) { if (texts.length > 0) { throw new UsageError('', 'fatal: batch modes take no arguments\n\n' + verbUsage(ctx)); } const value = fl.raw(batch); const template = typeof value === 'string' ? value : BATCH_FORMAT; for (const found of template.matchAll(FORMAT_ATOM)) { if (!BATCH_ATOMS.has(found[1] ?? '')) { throw new GitError(`bad cat-file format: ${found[0]}`); } } const repo = await opened(ctx, fl); const text = DEC.decode((await readStdinAsync(ctx.stdin)) ?? new Uint8Array()); const lines = text.split('\n'); if (lines.at(-1) === '') lines.pop(); const heads = []; for (const line of lines) heads.push(await batchHead(repo, line, template)); return [batchLines(repo, heads, batch === 'batch'), new IOResult()]; } if (first === undefined && texts.length === 0) throw new UsageError('', verbUsage(ctx)); if (first !== undefined) { const name = texts[0]; if (name === undefined) { throw new UsageError('', `fatal: <object> required with '-${first}'\n\n` + verbUsage(ctx)); } if (texts.length > 1) { throw new UsageError('', 'fatal: too many arguments\n\n' + verbUsage(ctx)); } const repo = await opened(ctx, fl); const [out, code] = await query(repo, first, name); return [out.length > 0 ? out : null, new IOResult({ exitCode: code })]; } const [kind, name] = texts; if (kind === undefined || name === undefined || texts.length !== 2) { throw new UsageError('', 'fatal: only two arguments allowed in <type> <object> mode, ' + `not ${String(texts.length)}\n\n` + verbUsage(ctx)); } if (!OBJECT_TYPES.some((type) => type === kind)) { throw new GitError(`invalid object type "${kind}"`); } const repo = await opened(ctx, fl); const shown = await typedBytes(repo, name, kind); if (shown === null) throw new GitError(`git cat-file ${name}: bad file`); return [shown, new IOResult()]; } //# sourceMappingURL=cat_file.js.map