Refactor UI modules and harden run/API behavior
This commit is contained in:
116
src/agents/manifest-store.ts
Normal file
116
src/agents/manifest-store.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
import { mkdir, readdir, readFile, writeFile } from "node:fs/promises";
|
||||
import { dirname, extname, isAbsolute, relative, resolve, sep } from "node:path";
|
||||
import { parseAgentManifest, type AgentManifest } from "../agents/manifest.js";
|
||||
|
||||
export type ManifestRecord = {
|
||||
path: string;
|
||||
manifest: AgentManifest;
|
||||
source: unknown;
|
||||
};
|
||||
|
||||
export type ManifestListing = {
|
||||
paths: string[];
|
||||
};
|
||||
|
||||
async function walkJsonFiles(root: string): Promise<string[]> {
|
||||
const output: string[] = [];
|
||||
|
||||
const entries = await readdir(root, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
const fullPath = resolve(root, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
output.push(...(await walkJsonFiles(fullPath)));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (entry.isFile() && extname(entry.name).toLowerCase() === ".json") {
|
||||
output.push(fullPath);
|
||||
}
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
function assertWorkspacePath(workspaceRoot: string, inputPath: string): string {
|
||||
const resolved = isAbsolute(inputPath)
|
||||
? resolve(inputPath)
|
||||
: resolve(workspaceRoot, inputPath);
|
||||
const rel = relative(workspaceRoot, resolved);
|
||||
|
||||
if (rel === ".." || rel.startsWith(`..${sep}`)) {
|
||||
throw new Error("Path is outside workspace root.");
|
||||
}
|
||||
|
||||
return resolved;
|
||||
}
|
||||
|
||||
function toRelativePath(workspaceRoot: string, absolutePath: string): string {
|
||||
return relative(workspaceRoot, absolutePath) || ".";
|
||||
}
|
||||
|
||||
export class ManifestStore {
|
||||
private readonly workspaceRoot: string;
|
||||
private readonly manifestDirectory: string;
|
||||
|
||||
constructor(input: { workspaceRoot: string; manifestDirectory?: string }) {
|
||||
this.workspaceRoot = resolve(input.workspaceRoot);
|
||||
this.manifestDirectory = assertWorkspacePath(
|
||||
this.workspaceRoot,
|
||||
input.manifestDirectory ?? ".ai_ops/manifests",
|
||||
);
|
||||
}
|
||||
|
||||
getManifestDirectory(): string {
|
||||
return this.manifestDirectory;
|
||||
}
|
||||
|
||||
async list(): Promise<ManifestListing> {
|
||||
try {
|
||||
const files = await walkJsonFiles(this.manifestDirectory);
|
||||
const relPaths = files
|
||||
.map((filePath) => toRelativePath(this.workspaceRoot, filePath))
|
||||
.sort((left, right) => left.localeCompare(right));
|
||||
|
||||
return {
|
||||
paths: relPaths,
|
||||
};
|
||||
} catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
||||
return {
|
||||
paths: [],
|
||||
};
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async read(pathInput: string): Promise<ManifestRecord> {
|
||||
const absolutePath = assertWorkspacePath(this.workspaceRoot, pathInput);
|
||||
const sourceText = await readFile(absolutePath, "utf8");
|
||||
const source = JSON.parse(sourceText) as unknown;
|
||||
const manifest = parseAgentManifest(source);
|
||||
|
||||
return {
|
||||
path: toRelativePath(this.workspaceRoot, absolutePath),
|
||||
manifest,
|
||||
source,
|
||||
};
|
||||
}
|
||||
|
||||
async validate(source: unknown): Promise<AgentManifest> {
|
||||
return parseAgentManifest(source);
|
||||
}
|
||||
|
||||
async save(pathInput: string, source: unknown): Promise<ManifestRecord> {
|
||||
const manifest = parseAgentManifest(source);
|
||||
const absolutePath = assertWorkspacePath(this.workspaceRoot, pathInput);
|
||||
await mkdir(dirname(absolutePath), { recursive: true });
|
||||
await writeFile(absolutePath, `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
|
||||
|
||||
return {
|
||||
path: toRelativePath(this.workspaceRoot, absolutePath),
|
||||
manifest,
|
||||
source: manifest,
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user