Files
AgentCompany/src/main/services/skill-artifacts.ts
T
ZacharyZhang-NY 07d0b4587d Promote AgentCompany-New to the repository root and retire the old app
The legacy application - its src, tests, landing site, promo assets, docs,
release tooling, CI workflows, and root configs - leaves the tree; it stays
reachable in history. Everything under AgentCompany-New moves up unchanged:
same package name, so the production userData/database path is untouched,
and e2e/build scripts already resolved paths relative to the project. The
developer cred.txt stays gitignored at its new location. Full verify:ci
passes from the new root.
2026-07-07 00:04:13 -04:00

86 lines
3.0 KiB
TypeScript

import { copyFileSync, existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, statSync } from "node:fs";
import { createHash } from "node:crypto";
import { dirname, isAbsolute, join, normalize, relative, resolve, sep } from "node:path";
export interface CopiedSkillFile {
relativePath: string;
objectKey: string;
sha256: string;
sizeBytes: number;
}
export function copySkillRevision(sourceDir: string, artifactRoot: string, revisionId: string): CopiedSkillFile[] {
const sourceRoot = resolve(sourceDir);
if (!existsSync(sourceRoot) || !lstatSync(sourceRoot).isDirectory()) {
throw new Error("Skill directory was not found.");
}
const targetRoot = join(artifactRoot, "skills", revisionId);
const files = listSkillFiles(sourceRoot);
mkdirSync(targetRoot, { recursive: true });
return files.map((relativePath) => {
const sourcePath = pathFromRelative(sourceRoot, relativePath);
const targetPath = pathFromRelative(targetRoot, relativePath);
mkdirSync(dirname(targetPath), { recursive: true });
copyFileSync(sourcePath, targetPath);
return {
relativePath,
objectKey: targetPath,
sha256: fileChecksum(targetPath),
sizeBytes: statSync(targetPath).size,
};
});
}
export function recursiveSkillChecksum(sourceDir: string): string {
const sourceRoot = resolve(sourceDir);
const hash = createHash("sha256");
for (const relativePath of listSkillFiles(sourceRoot)) {
hash.update(relativePath);
hash.update(readFileSync(pathFromRelative(sourceRoot, relativePath)));
}
return hash.digest("hex");
}
export function assertSafeSkillPath(relativePath: string): string {
const clean = relativePath.trim().replaceAll("\\", "/");
if (!clean || isAbsolute(clean)) throw new Error(`Invalid skill artifact path: ${relativePath}`);
const normalized = normalize(clean).replaceAll("\\", "/");
if (normalized === "." || normalized.startsWith("../") || normalized.includes("/../")) {
throw new Error(`Invalid skill artifact path: ${relativePath}`);
}
return normalized;
}
function listSkillFiles(root: string): string[] {
const files: string[] = [];
walk(root, root, files);
return files.sort();
}
function walk(root: string, currentDir: string, files: string[]): void {
for (const name of readdirSync(currentDir).sort()) {
if (name.startsWith(".")) continue;
const path = join(currentDir, name);
const stat = lstatSync(path);
if (stat.isSymbolicLink()) throw new Error("Skill directories must not contain symbolic links.");
if (stat.isDirectory()) {
walk(root, path, files);
continue;
}
if (stat.isFile()) files.push(relativePath(root, path));
}
}
function relativePath(root: string, path: string): string {
return relative(root, path).split(sep).join("/");
}
function pathFromRelative(root: string, relativePath: string): string {
const safe = assertSafeSkillPath(relativePath);
return join(root, ...safe.split("/"));
}
function fileChecksum(path: string): string {
return createHash("sha256").update(readFileSync(path)).digest("hex");
}