* fix(ci): skip unrelated required checks cleanly Matrix jobs had to start no-op runners to preserve interpolated required-check names. Static named jobs let GitHub report genuine skipped conclusions while keeping those names stable. * fix(ci): gate changes to install patching script * fix(ci): route cross-package test contracts narrowly * fix(ci): keep routed contracts merge-blocking * fix(server): exclude nested test utilities from builds * fix(ci): cover app-owned native and browser domains * refactor(ci): align tests with runtime domains Own browser specs, desktop specs, and Electron-only app code by stable directories so PR routing does not depend on filename conventions. Keep desktop integration coverage inside the required desktop check. * fix(ci): keep routing and desktop suites isolated The routing regression must run before dependency installation, and desktop Vitest must not collect Playwright-owned specs. * fix(ci): isolate desktop e2e runtime * perf(test): isolate Playwright workers * perf(test): parallelize Playwright within specs * fix(test): keep Playwright file scheduling * fix(test): share dynamic Metro fixture * fix(test): preserve own-host restart fixtures * fix(ci): preserve routed contracts after rebase * Stabilize Playwright wheel-scroll assertions
132 lines
4.0 KiB
TypeScript
132 lines
4.0 KiB
TypeScript
import { existsSync } from "node:fs";
|
|
import { copyFile, mkdir, readdir, rm, stat } from "node:fs/promises";
|
|
import { homedir } from "node:os";
|
|
import path from "node:path";
|
|
|
|
export interface PaseoHomeMetadataForkResult {
|
|
sourceHome: string;
|
|
targetHome: string;
|
|
agentFiles: number;
|
|
agentBytes: number;
|
|
projectFiles: number;
|
|
projectBytes: number;
|
|
copiedFiles: number;
|
|
copiedBytes: number;
|
|
skippedMissing: string[];
|
|
}
|
|
|
|
interface CopyStats {
|
|
files: number;
|
|
bytes: number;
|
|
skippedMissing: string[];
|
|
}
|
|
|
|
export function resolvePaseoHomePath(value: string): string {
|
|
if (value === "~") {
|
|
return homedir();
|
|
}
|
|
if (value.startsWith("~/")) {
|
|
return path.join(homedir(), value.slice(2));
|
|
}
|
|
return path.resolve(value);
|
|
}
|
|
|
|
async function copyJsonTree(sourceDir: string, targetDir: string): Promise<CopyStats> {
|
|
if (!existsSync(sourceDir)) {
|
|
return { files: 0, bytes: 0, skippedMissing: [sourceDir] };
|
|
}
|
|
|
|
const stats: CopyStats = { files: 0, bytes: 0, skippedMissing: [] };
|
|
const entries = await readdir(sourceDir, { withFileTypes: true });
|
|
await mkdir(targetDir, { recursive: true });
|
|
|
|
for (const entry of entries) {
|
|
const sourcePath = path.join(sourceDir, entry.name);
|
|
const targetPath = path.join(targetDir, entry.name);
|
|
|
|
if (entry.isDirectory()) {
|
|
const nested = await copyJsonTree(sourcePath, targetPath);
|
|
stats.files += nested.files;
|
|
stats.bytes += nested.bytes;
|
|
stats.skippedMissing.push(...nested.skippedMissing);
|
|
continue;
|
|
}
|
|
|
|
if (!entry.isFile() || !entry.name.endsWith(".json")) {
|
|
continue;
|
|
}
|
|
|
|
await mkdir(path.dirname(targetPath), { recursive: true });
|
|
await copyFile(sourcePath, targetPath);
|
|
const fileStat = await stat(sourcePath);
|
|
stats.files += 1;
|
|
stats.bytes += fileStat.size;
|
|
}
|
|
|
|
return stats;
|
|
}
|
|
|
|
async function copyProjectRegistryFiles(
|
|
sourceHome: string,
|
|
targetHome: string,
|
|
): Promise<CopyStats> {
|
|
const stats: CopyStats = { files: 0, bytes: 0, skippedMissing: [] };
|
|
const sourceProjectsDir = path.join(sourceHome, "projects");
|
|
const targetProjectsDir = path.join(targetHome, "projects");
|
|
await mkdir(targetProjectsDir, { recursive: true });
|
|
|
|
for (const fileName of ["projects.json", "workspaces.json"]) {
|
|
const sourcePath = path.join(sourceProjectsDir, fileName);
|
|
const targetPath = path.join(targetProjectsDir, fileName);
|
|
if (!existsSync(sourcePath)) {
|
|
stats.skippedMissing.push(sourcePath);
|
|
continue;
|
|
}
|
|
await copyFile(sourcePath, targetPath);
|
|
const fileStat = await stat(sourcePath);
|
|
stats.files += 1;
|
|
stats.bytes += fileStat.size;
|
|
}
|
|
|
|
return stats;
|
|
}
|
|
|
|
export async function forkPaseoHomeMetadata(input: {
|
|
sourceHome: string;
|
|
targetHome: string;
|
|
}): Promise<PaseoHomeMetadataForkResult> {
|
|
const sourceHome = resolvePaseoHomePath(input.sourceHome);
|
|
const targetHome = resolvePaseoHomePath(input.targetHome);
|
|
|
|
if (sourceHome === targetHome) {
|
|
throw new Error("Refusing to fork Paseo metadata onto the same PASEO_HOME.");
|
|
}
|
|
|
|
await mkdir(targetHome, { recursive: true });
|
|
|
|
// Reset only the copied metadata surface. In particular, do not copy or remove
|
|
// worktrees here: forked workspace records should continue to point at the
|
|
// original checkout/worktree paths from the source home.
|
|
await rm(path.join(targetHome, "agents"), { recursive: true, force: true });
|
|
await rm(path.join(targetHome, "projects", "projects.json"), { force: true });
|
|
await rm(path.join(targetHome, "projects", "workspaces.json"), { force: true });
|
|
|
|
const agents = await copyJsonTree(
|
|
path.join(sourceHome, "agents"),
|
|
path.join(targetHome, "agents"),
|
|
);
|
|
const projects = await copyProjectRegistryFiles(sourceHome, targetHome);
|
|
|
|
return {
|
|
sourceHome,
|
|
targetHome,
|
|
agentFiles: agents.files,
|
|
agentBytes: agents.bytes,
|
|
projectFiles: projects.files,
|
|
projectBytes: projects.bytes,
|
|
copiedFiles: agents.files + projects.files,
|
|
copiedBytes: agents.bytes + projects.bytes,
|
|
skippedMissing: [...agents.skippedMissing, ...projects.skippedMissing],
|
|
};
|
|
}
|