/** * validate.ts — Structural (Zod) + business-rule validation. No side effects. */ import { InitSpecSchema, type InitSpec } from './types.js'; export interface ValidationResult { valid: boolean; data?: InitSpec; blockers: string[]; warnings: string[]; } export function validate(raw: unknown): ValidationResult { const blockers: string[] = []; const warnings: string[] = []; // ── Structural validation (Zod) ───────────────────────── const parsed = InitSpecSchema.safeParse(raw); if (!parsed.success) { for (const issue of parsed.error.issues) { blockers.push(`[${issue.path.join('.')}] ${issue.message}`); } return { valid: false, blockers, warnings }; } const data = parsed.data; // ── Business rules ────────────────────────────────────── // A URL is required to build (proposal mode may still echo the derived name). if (data.url) { const looksRemote = /^(https?:\/\/|git@|ssh:\/\/)/.test(data.url) || data.url.includes('dev.azure.com'); if (!looksRemote) { warnings.push(`URL does not look like a git remote: ${data.url}`); } if ( !data.url.includes('github.com') && !data.url.includes('dev.azure.com') && !data.url.includes('gitlab.com') && !data.url.startsWith('git@') && !/^[A-Za-z]:[\\/]/.test(data.url) // allow a local path as a test/import source ) { warnings.push(`URL provider not recognized: ${data.url}`); } } // Project name must be a safe folder/repo name. if (data.name && !/^[a-z0-9][a-z0-9._-]*$/i.test(data.name)) { blockers.push(`Invalid project name: ${data.name}`); } return { valid: blockers.length === 0, data, blockers, warnings }; }