A self-hosted orchestration system crafted for power users. composia.xyz
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260import type { RepoFileEntry, RepoWriteResult } from "$lib/server/controller";import { resolveMaterialIconNames } from "$lib/server/vscode-material-icons";import type { ServiceFileNode, WorkspaceFile } from "$lib/service-workspace";import { createRepoDirectory, deleteRepoPath, loadRepoEntries, loadRepoFile, loadSystemCapabilities, loadSecret, moveRepoPath, updateRepoFile, updateSecret,} from "$lib/server/controller";import { isEncryptedFilePath, normalizeServiceRelativePath,} from "$lib/service-workspace";
const ENCRYPTED_FILE_REASON_MESSAGES: Record<string, string> = { missing_secrets_config: "Secrets configuration is incomplete.",};
export async function loadServiceFileTree( serviceDir: string,): Promise<ServiceFileNode[]> { const entries = await loadRepoEntries( repoPathForServicePath(serviceDir, ""), { recursive: true, }, ); return buildServiceFileTree(serviceDir, entries);}
export async function loadServiceWorkspaceFile( serviceDir: string, relativePath: string,): Promise<WorkspaceFile> { const normalized = normalizeServiceRelativePath(relativePath); let content: string; if (isEncryptedFilePath(normalized)) { const capabilities = await loadSystemCapabilities(); if (!capabilities.global.secrets.enabled) { return unavailableEncryptedWorkspaceFile( normalized, capabilities.global.secrets.reasonCode || "missing_secrets_config", ); }
const secret = await loadSecret(serviceDir, normalized); content = secret.content ?? ""; } else { const file = await loadRepoFile( repoPathForServicePath(serviceDir, normalized), ); content = file.content ?? ""; } return { path: normalized, content, size: content.length, };}
export async function saveServiceWorkspaceFile( serviceDir: string, relativePath: string, content: string, baseRevision: string,): Promise<{ file: WorkspaceFile; write: RepoWriteResult }> { const normalized = normalizeServiceRelativePath(relativePath); let write: RepoWriteResult; if (isEncryptedFilePath(normalized)) { const capabilities = await loadSystemCapabilities(); if (!capabilities.global.secrets.enabled) { throw new Error( ENCRYPTED_FILE_REASON_MESSAGES[ capabilities.global.secrets.reasonCode || "missing_secrets_config" ] ?? "Encrypted file is currently unavailable.", ); }
write = await updateSecret(serviceDir, normalized, content, baseRevision); } else { write = await updateRepoFile( repoPathForServicePath(serviceDir, normalized), content, baseRevision, ); } return { file: { path: normalized, content, size: content.length, }, write, };}
function unavailableEncryptedWorkspaceFile( path: string, unavailableReasonCode: string,): WorkspaceFile { return { path, content: "", size: 0, readOnly: true, unavailableReasonCode, };}
export async function createServiceWorkspaceDirectory( serviceDir: string, relativePath: string, baseRevision: string,): Promise<RepoWriteResult> { const normalized = normalizeServiceRelativePath(relativePath); return createRepoDirectory( repoPathForServicePath(serviceDir, normalized), baseRevision, );}
export async function moveServiceWorkspacePath( serviceDir: string, sourcePath: string, destinationPath: string, baseRevision: string,): Promise<RepoWriteResult> { const normalizedSource = normalizeServiceRelativePath(sourcePath); const normalizedDestination = normalizeServiceRelativePath(destinationPath); return moveRepoPath( repoPathForServicePath(serviceDir, normalizedSource), repoPathForServicePath(serviceDir, normalizedDestination), baseRevision, );}
export async function deleteServiceWorkspacePath( serviceDir: string, relativePath: string, baseRevision: string,): Promise<RepoWriteResult> { const normalized = normalizeServiceRelativePath(relativePath); return deleteRepoPath( repoPathForServicePath(serviceDir, normalized), baseRevision, );}
function buildServiceFileTree( serviceDir: string, entries: RepoFileEntry[],): ServiceFileNode[] { const root: ServiceFileNode[] = []; const directories = new Map<string, ServiceFileNode>();
for (const entry of entries) { if (entry.name === ".gitkeep") { continue; }
const relativePath = serviceRelativePath(serviceDir, entry.path); if (!relativePath) { continue; }
const icons = resolveMaterialIconNames(relativePath, entry.isDir);
const node: ServiceFileNode = { name: entry.name, path: relativePath, isDir: entry.isDir, iconName: icons.iconName ?? undefined, lightIconName: icons.lightIconName ?? undefined, expandedIconName: icons.expandedIconName ?? undefined, expandedLightIconName: icons.expandedLightIconName ?? undefined, children: [], };
if (entry.isDir) { directories.set(relativePath, node); }
const parentPath = parentServiceRelativePath(relativePath); if (!parentPath) { root.push(node); continue; }
const parent = directories.get(parentPath); if (!parent) { throw new Error( `Repo path ${entry.path} is missing parent ${parentPath}.`, ); } parent.children.push(node); }
sortServiceFileNodes(root); return root;}
function sortServiceFileNodes(nodes: ServiceFileNode[]) { nodes.sort((left, right) => { if (left.isDir !== right.isDir) { return left.isDir ? -1 : 1; } return left.name.localeCompare(right.name); }); for (const node of nodes) { if (node.children.length > 0) { sortServiceFileNodes(node.children); } }}
export function repoPathForServicePath( serviceDir: string, relativePath: string,) { const normalizedDir = serviceDir === "." ? "" : normalizeServiceRelativePath(serviceDir); const normalizedPath = normalizeServiceRelativePath(relativePath); if (!normalizedDir) { return normalizedPath; } if (!normalizedPath) { return normalizedDir; } return `${normalizedDir}/${normalizedPath}`;}
function serviceRelativePath(serviceDir: string, repoPath: string) { const normalizedDir = serviceDir === "." ? "" : normalizeServiceRelativePath(serviceDir); const normalizedRepoPath = normalizeServiceRelativePath(repoPath); if (!normalizedDir) { return normalizedRepoPath; } if (normalizedRepoPath === normalizedDir) { return ""; } if (!normalizedRepoPath.startsWith(`${normalizedDir}/`)) { throw new Error(`Repo path ${repoPath} is outside service ${serviceDir}.`); } return normalizedRepoPath.slice(normalizedDir.length + 1);}
function parentServiceRelativePath(path: string) { const normalized = normalizeServiceRelativePath(path); if (!normalized || !normalized.includes("/")) { return ""; } return normalized.slice(0, normalized.lastIndexOf("/"));}