ci/woodpecker/push/publish Pipeline was canceled
Co-authored-by: fargo <[email protected]>
115 lines
3.3 KiB
TypeScript
115 lines
3.3 KiB
TypeScript
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<string, PrdSectionPatch>();
|
|
|
|
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<string> {
|
|
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<string> {
|
|
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<PrdDocument> {
|
|
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;
|
|
}
|