@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
131 lines • 5.61 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 { GitHubAccessor, GitHubAccount } from "../../accessor/github.js";
import { fetchRepoInfo as fetchGitHubRepoInfo } from "../../core/github/client.js";
import { GitHubConfig } from "../../core/github/config.js";
import { COMMIT_SHA, WALK_LIMIT } from "../../core/github/constants.js";
import { readMany as githubReadMany, readStream as githubStream } from "../../core/github/read.js";
import { readdir as githubReaddir } from "../../core/github/readdir.js";
import { filesContaining as githubFilesContaining } from "../../core/github/search.js";
import { stat as githubStat, tree as githubTree } from "../../core/github/stat.js";
import { fetchTree as fetchGitHubTree, buildTreeMap as githubBuildTreeMap, } from "../../core/github/tree.js";
import {} from "../../io/types.js";
import { ListingVersion, VFSName } from "../../types.js";
import { BaseVFS } from "../base.js";
/**
* The commit a ref pins every listing at, when it names one outright. Only a
* full 40- or 64-hex string can be a commit; GitHub answers shas lowercase,
* so the pin is lowercased to compare with what it stores. Listings are
* stored at the head their tree answered, so a ref is served unchecked only
* when its listing was fetched at that sha. github.com refuses a branch or
* tag named with 40 or 64 hex characters (HTTP 422), so such a ref always
* names a commit, which cannot move; a GitHub Enterprise host behind
* `baseUrl` is assumed to refuse them too.
*/
function pinOf(ref) {
if (ref === undefined)
return null;
const lowered = ref.toLowerCase();
return COMMIT_SHA.test(lowered) ? lowered : null;
}
export class GitHubVFS extends BaseVFS {
name = VFSName.GITHUB;
cachesReads = true;
// The git tree API reports the exact blob size for every file; the
// blob read returns those same bytes, and submodule gitlinks (which
// have no size and no blob) are excluded from the tree.
sizesAlwaysKnown = true;
// stat and a read both stamp the content-addressed blob sha.
supportsSnapshot = true;
readRevalidatable = true;
// One version covers every listing: the head commit the ref resolves to,
// which the tree response names as its top-level sha and the root stat
// answers with one shallow request.
listingVersion = ListingVersion.MOUNT;
listingsPin;
indexTtl = 86_400;
wholeWordSearch = true;
config;
accessor;
walkLimit = WALK_LIMIT;
constructor(config, accessor) {
super();
this.config = config;
this.accessor = accessor;
this.listingsPin = pinOf(config.ref);
}
static async create(config) {
const account = new GitHubAccount(config.baseUrl === undefined
? { token: config.token }
: { token: config.token, baseUrl: config.baseUrl });
const repoInfo = await fetchGitHubRepoInfo(account.transport, config.owner, config.repo);
const ref = config.ref ?? repoInfo.default_branch;
const { tree, truncated } = await fetchGitHubTree(account.transport, config.owner, config.repo, ref);
const treeMap = githubBuildTreeMap(tree);
const accessor = new GitHubAccessor({
account,
owner: config.owner,
repo: config.repo,
ref,
defaultBranch: repoInfo.default_branch,
truncated,
tree: treeMap,
});
return new GitHubVFS(config, accessor);
}
handed = {
readdir: ['entry'],
read: ['entry'],
readMany: ['entry'],
stat: ['entry', 'index'],
};
readdir(path, handed) {
return githubReaddir(this.accessor, path, handed?.entry);
}
// eslint-disable-next-line @typescript-eslint/require-await
async read(path, handed) {
return githubStream(this.accessor, path, handed?.entry);
}
readMany(paths, limit, handed) {
return githubReadMany(this.accessor, paths, limit, handed);
}
stat(path, handed) {
return githubStat(this.accessor, path, handed?.entry, handed?.index);
}
readTree(path) {
return githubTree(this.accessor, path);
}
/**
* Whether this VFS answers `name`; with no walk limit it reads every
* file rather than ask the service's search.
*/
supports(name) {
if (name === 'files_containing' && this.walkLimit < 0)
return false;
return super.supports(name);
}
filesContaining(path, text, opts) {
if (!opts.wholeWord || this.walkLimit < 0)
return Promise.resolve(null);
return githubFilesContaining(this.accessor, text, [path]);
}
getState() {
return Promise.resolve({
...this.configState(GitHubConfig, this.config),
defaultBranch: this.accessor.defaultBranch,
truncated: this.accessor.truncated,
});
}
}
//# sourceMappingURL=github.js.map