import path from 'node:path'; import { cancel, intro, isCancel, outro, select, text } from '@clack/prompts'; import { PrdService } from './service.js'; import type { CreatePrdOptions, PrdDocument, PrdSectionPatch } from './types.js'; interface WizardAnswers { goals: string; constraints: string; milestones: string; } /** * Translate wizard answers into section patches using the same keyword * matching the wizard always used (first section whose id contains the * keyword, then first field whose name contains it, else first field). */ function buildWizardPatches(doc: PrdDocument, answers: WizardAnswers): PrdSectionPatch[] { const bySection = new Map(); const add = (keyword: string, value: string): void => { const section = doc.sections.find((candidate) => candidate.id.includes(keyword)); if (section === undefined) { return; } const fieldName = Object.keys(section.fields).find((field) => field.toLowerCase().includes(keyword)) ?? Object.keys(section.fields)[0]; if (fieldName === undefined || section.fields[fieldName] === value) { return; } const existing = bySection.get(section.id); if (existing === undefined) { bySection.set(section.id, { id: section.id, fields: { [fieldName]: value } }); } else { existing.fields[fieldName] = value; } }; add('goal', answers.goals); add('constraint', answers.constraints); add('milestone', answers.milestones); return [...bySection.values()]; } async function promptText(message: string, initialValue = ''): Promise { const response = await text({ message, initialValue, }); if (isCancel(response)) { cancel('PRD wizard cancelled.'); throw new Error('PRD wizard cancelled'); } return response.trim(); } async function promptTemplate(template?: string): Promise { if (template !== undefined && template.trim().length > 0) { return template; } const choice = await select({ message: 'PRD type', options: [ { value: 'software', label: 'Software project' }, { value: 'feature', label: 'Feature' }, { value: 'spike', label: 'Research spike' }, ], }); if (isCancel(choice)) { cancel('PRD wizard cancelled.'); throw new Error('PRD wizard cancelled'); } return choice; } /** * Interactive PRD wizard. All writes go through PrdService — the wizard is a * prompt layer, never a second writer path. */ export async function runPrdWizard(options: CreatePrdOptions): Promise { intro('Mosaic PRD wizard'); const name = options.name.trim().length > 0 ? options.name.trim() : await promptText('Project name'); const template = await promptTemplate(options.template); const goals = await promptText('Primary goals'); const constraints = await promptText('Key constraints'); const milestones = await promptText('Planned milestones'); const service = new PrdService({ projectPath: options.projectPath }); const doc = await service.create({ name, template, }); const patches = buildWizardPatches(doc, { goals, constraints, milestones }); const updated = patches.length > 0 ? await service.update({ id: doc.id, sections: patches }) : doc; outro(`PRD created: ${path.join(updated.projectPath, 'docs', 'prdy', `${updated.id}.yaml`)}`); return updated; }