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.
86 lines
3.0 KiB
TypeScript
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");
|
|
}
|