/** * start CLI validate — Pure validation, no I/O */ import { StartSpecSchema, type StartSpec } from './types.js'; export interface ValidationResult { valid: boolean; data?: StartSpec; blockers: string[]; warnings: string[]; } export function validate(spec: unknown): ValidationResult { const blockers: string[] = []; const warnings: string[] = []; let data: StartSpec | undefined; try { data = StartSpecSchema.parse(spec); } catch (err: unknown) { const e = err as any; if (e.errors) { for (const error of e.errors) { blockers.push(`${error.path?.join('.')}: ${error.message}`); } } else { blockers.push(`Spec parse failed: ${(err as Error).message}`); } return { valid: false, blockers, warnings }; } if (!data.name || data.name.trim().length === 0) { blockers.push('name cannot be empty'); } const nameTrimmed = data.name.trim(); if (!/^[a-zA-Z0-9._\-]+$/.test(nameTrimmed)) { blockers.push(`name contains invalid characters: ${nameTrimmed}`); } if (nameTrimmed.length > 100) { blockers.push('name too long (max 100 characters)'); } // A release/hotfix name IS the version it will tag — require semver so the // tag, the sources and the branch name all agree (Model B). Optional leading // `v` and a -prerelease/+build suffix are tolerated. if (data.type === 'release' || data.type === 'hotfix') { const v = nameTrimmed.replace(/^v/i, ''); if (!/^\d+\.\d+\.\d+([-.+][0-9A-Za-z.-]+)?$/.test(v)) { blockers.push( `${data.type} version must be semver (e.g. 5.1.0), got: "${nameTrimmed}". ` + `The name becomes the tag and the version written to the sources.`, ); } } return { valid: blockers.length === 0, data, blockers, warnings }; }