Intelligence
Artifacts
Browse the repository, read documents, and manage the governance folders. Source, runtime, and infrastructure are read-only.
Repository
repositories/aaf-holdings/hq01/lib/content/artifacts.ts
3.3 KB
import fs from "node:fs";
import path from "node:path";
import { cache } from "react";
import { CONTENT_ROOT, IGNORED_DIRS } from "./config";
import type { ArtifactNode } from "./types";
const TEXT_EXTENSIONS = new Set([
".md", ".mdx", ".txt", ".yaml", ".yml", ".json", ".ts", ".tsx", ".js",
".jsx", ".css", ".scss", ".html", ".sh", ".env", ".toml", ".ini", ".csv",
".sql", ".py", ".rb", ".go", ".rs", ".gitignore", ".dockerfile",
]);
function buildTree(dir: string, rel: string, depth: number): ArtifactNode[] {
if (depth > 8) return [];
let entries: fs.Dirent[];
try {
entries = fs.readdirSync(dir, { withFileTypes: true });
} catch {
return [];
}
const nodes: ArtifactNode[] = [];
for (const entry of entries) {
if (entry.name.startsWith(".") && entry.name !== ".gitignore") continue;
if (IGNORED_DIRS.has(entry.name)) continue;
// Never surface the HQ01 app's own build output as content.
if (entry.name === "hq01" && rel === "repositories/aaf-holdings") {
// still allow browsing source, but skip heavy generated dirs handled above
}
const full = path.join(dir, entry.name);
const childRel = rel ? `${rel}/${entry.name}` : entry.name;
if (entry.isDirectory()) {
nodes.push({
name: entry.name,
path: childRel,
type: "dir",
children: buildTree(full, childRel, depth + 1),
});
} else if (entry.isFile()) {
let size: number | undefined;
try {
size = fs.statSync(full).size;
} catch {
size = undefined;
}
nodes.push({
name: entry.name,
path: childRel,
type: "file",
ext: path.extname(entry.name).toLowerCase(),
size,
});
}
}
return nodes.sort((a, b) => {
if (a.type !== b.type) return a.type === "dir" ? -1 : 1;
return a.name.localeCompare(b.name);
});
}
export const getArtifactTree = cache((): ArtifactNode[] => {
return buildTree(CONTENT_ROOT, "", 0);
});
/** Resolve a workspace-relative path safely (no escaping the content root). */
function resolveSafe(relativePath: string): string | null {
const normalized = path
.normalize(relativePath)
.replace(/^([/\\])+/, "");
const full = path.join(CONTENT_ROOT, normalized);
const rootWithSep = CONTENT_ROOT.endsWith(path.sep)
? CONTENT_ROOT
: CONTENT_ROOT + path.sep;
if (full !== CONTENT_ROOT && !full.startsWith(rootWithSep)) return null;
return full;
}
export interface ArtifactFile {
path: string;
name: string;
ext: string;
size: number;
isText: boolean;
content: string | null;
}
export const getArtifactFile = cache((relativePath: string): ArtifactFile | null => {
const full = resolveSafe(relativePath);
if (!full) return null;
let stat: fs.Stats;
try {
stat = fs.statSync(full);
} catch {
return null;
}
if (!stat.isFile()) return null;
const ext = path.extname(full).toLowerCase();
const name = path.basename(full);
const isText =
TEXT_EXTENSIONS.has(ext) ||
TEXT_EXTENSIONS.has(name.toLowerCase()) ||
stat.size < 256 * 1024;
let content: string | null = null;
if (isText && stat.size < 1024 * 1024) {
try {
content = fs.readFileSync(full, "utf8");
} catch {
content = null;
}
}
return {
path: relativePath,
name,
ext,
size: stat.size,
isText: Boolean(content !== null),
content,
};
});
root · /srv/aaf