import { type AppManifest, isAppManifest } from '@ankhorage/contracts'; import { promises as fs } from 'fs'; import path from 'path'; import type { ResolvedInfraProject } from '../../../../types/infraProject.js'; /*** Resolve one manifest-backed Infra project from the workspace and optional project selector. */ export async function resolveInfraProjectAsync(options: { readonly cwd: string; readonly projectId?: string; }): Promise { const workspaceRoot = await findWorkspaceRoot(options.cwd); if (workspaceRoot === null) { throw new Error(`Could not find an Ankh workspace root from cwd: ${options.cwd}`); } const appsRoot = path.join(workspaceRoot, 'apps'); const projectId = options.projectId === undefined ? inferProjectId(options.cwd, appsRoot) : validateExplicitProjectId(options.projectId); if (projectId === null) { throw new Error( 'Could not infer a project from cwd. Pass a project id or run the command inside apps/.', ); } const projectPath = resolveProjectPath(appsRoot, projectId); if (!(await pathExists(projectPath))) { throw new Error(`Project '${projectId}' not found.`); } const manifestPath = path.join(projectPath, 'ankh.config.json'); const manifest = await readProjectManifest(manifestPath); return { appsRoot, manifest, manifestPath, projectId, projectPath, workspaceRoot, }; } function resolveProjectPath(appsRoot: string, projectId: string): string { const projectPath = path.resolve(appsRoot, projectId); const appsRootPath = `${path.resolve(appsRoot)}${path.sep}`; if (!projectPath.startsWith(appsRootPath)) { throw new Error('Security check failed while resolving the project path.'); } return projectPath; } async function readProjectManifest(manifestPath: string): Promise { if (!(await pathExists(manifestPath))) { throw new Error(`Project manifest not found: ${manifestPath}`); } const rawManifest = await fs.readFile(manifestPath, 'utf8'); let parsed: unknown; try { parsed = JSON.parse(rawManifest); } catch (error) { throw new Error( `Project manifest is not valid JSON: ${manifestPath}: ${getErrorMessage(error)}`, { cause: error }, ); } if (!isAppManifest(parsed)) { throw new Error(`Project manifest has an invalid shape: ${manifestPath}`); } return parsed; } async function findWorkspaceRoot(startPath: string): Promise { let currentPath = path.resolve(startPath); for (;;) { const appsRoot = path.join(currentPath, 'apps'); if (await isDirectory(appsRoot)) return currentPath; const parentPath = path.dirname(currentPath); if (parentPath === currentPath) return null; currentPath = parentPath; } } function inferProjectId(cwd: string, appsRoot: string): string | null { const relativePath = path.relative(path.resolve(appsRoot), path.resolve(cwd)); if (relativePath === '' || relativePath.startsWith('..') || path.isAbsolute(relativePath)) { return null; } const [projectId] = relativePath.split(path.sep); return projectId === undefined || projectId.length === 0 ? null : path.basename(projectId); } function validateExplicitProjectId(projectId: string): string { if (projectId.trim().length === 0) { throw new Error('Invalid project id: expected a single safe project segment.'); } if ( projectId === '.' || projectId === '..' || path.isAbsolute(projectId) || projectId.includes('/') || projectId.includes('\\') ) { throw new Error(`Invalid project id: ${projectId}. Expected a single safe project segment.`); } return projectId; } async function isDirectory(filePath: string): Promise { try { const stat = await fs.stat(filePath); return stat.isDirectory(); } catch { return false; } } async function pathExists(filePath: string): Promise { try { await fs.access(filePath); return true; } catch { return false; } } function getErrorMessage(error: unknown): string { return error instanceof Error ? error.message : String(error); }