- PrdService owns create/read/update/link/import/export; wizard and package CLI become prompt layers over the service (no second writer path) - PRD documents gain a content version and a missions linkage array persisted in the YAML authority store (survives restart); linkage writes do not bump the content version - exportMarkdown renders a labeled generated view (id + version + do-not-edit header) that no code path reads back; the store loads .yaml/.yml only - importDocument validates structure with zod before anything else, persists valid imports as draft (validity is not approval), and refuses conflicts with a typed PrdImportConflictError carrying a proposed successor; the original document stays byte-identical until acceptSuccessor - raw store writers (createPrd/savePrd) are no longer exported from the package entry point
161 lines
4.5 KiB
TypeScript
161 lines
4.5 KiB
TypeScript
import { Command } from 'commander';
|
|
|
|
import { PrdService } from './service.js';
|
|
import { runPrdWizard } from './wizard.js';
|
|
|
|
interface InitCommandOptions {
|
|
readonly name: string;
|
|
readonly project: string;
|
|
readonly template?: 'software' | 'feature' | 'spike';
|
|
}
|
|
|
|
interface ListCommandOptions {
|
|
readonly project: string;
|
|
}
|
|
|
|
interface ShowCommandOptions {
|
|
readonly project: string;
|
|
readonly id?: string;
|
|
}
|
|
|
|
interface ImportCommandOptions {
|
|
readonly project: string;
|
|
readonly file: string;
|
|
readonly acceptSuccessor?: boolean;
|
|
}
|
|
|
|
interface ExportCommandOptions {
|
|
readonly project: string;
|
|
readonly id?: string;
|
|
readonly out?: string;
|
|
}
|
|
|
|
function serviceFor(project: string): PrdService {
|
|
return new PrdService({ projectPath: project });
|
|
}
|
|
|
|
export function buildPrdyCli(): Command {
|
|
const program = new Command();
|
|
program.name('mosaic').description('Mosaic CLI').exitOverride();
|
|
|
|
const prdy = program.command('prdy').description('PRD wizard commands');
|
|
|
|
prdy
|
|
.command('init')
|
|
.description('Create a PRD document')
|
|
.requiredOption('--name <name>', 'PRD name')
|
|
.requiredOption('--project <path>', 'Project path')
|
|
.option('--template <template>', 'Template (software|feature|spike)')
|
|
.action(async (options: InitCommandOptions) => {
|
|
const doc = process.stdout.isTTY
|
|
? await runPrdWizard({
|
|
name: options.name,
|
|
projectPath: options.project,
|
|
template: options.template,
|
|
interactive: true,
|
|
})
|
|
: await serviceFor(options.project).create({
|
|
name: options.name,
|
|
template: options.template,
|
|
});
|
|
|
|
console.log(
|
|
JSON.stringify(
|
|
{
|
|
ok: true,
|
|
id: doc.id,
|
|
title: doc.title,
|
|
status: doc.status,
|
|
version: doc.version,
|
|
projectPath: doc.projectPath,
|
|
},
|
|
null,
|
|
2,
|
|
),
|
|
);
|
|
});
|
|
|
|
prdy
|
|
.command('list')
|
|
.description('List PRD documents for a project')
|
|
.requiredOption('--project <path>', 'Project path')
|
|
.action(async (options: ListCommandOptions) => {
|
|
const docs = await serviceFor(options.project).list();
|
|
console.log(JSON.stringify(docs, null, 2));
|
|
});
|
|
|
|
prdy
|
|
.command('show')
|
|
.description('Show a PRD document')
|
|
.requiredOption('--project <path>', 'Project path')
|
|
.option('--id <id>', 'PRD document id')
|
|
.action(async (options: ShowCommandOptions) => {
|
|
const doc = await serviceFor(options.project).get(options.id);
|
|
console.log(JSON.stringify(doc, null, 2));
|
|
});
|
|
|
|
prdy
|
|
.command('import')
|
|
.description('Import a YAML PRD document (validated; conflicts propose a successor)')
|
|
.requiredOption('--project <path>', 'Project path')
|
|
.requiredOption('--file <file>', 'Path to YAML PRD document')
|
|
.option('--accept-successor', 'Accept a conflicted import as the next version')
|
|
.action(async (options: ImportCommandOptions) => {
|
|
const service = serviceFor(options.project);
|
|
const input = { filePath: options.file };
|
|
|
|
if (options.acceptSuccessor) {
|
|
const successor = await service.acceptSuccessor(input);
|
|
console.log(
|
|
JSON.stringify(
|
|
{
|
|
ok: true,
|
|
outcome: 'successor-accepted',
|
|
id: successor.id,
|
|
version: successor.version,
|
|
},
|
|
null,
|
|
2,
|
|
),
|
|
);
|
|
return;
|
|
}
|
|
|
|
const result = await service.importDocument(input);
|
|
console.log(
|
|
JSON.stringify(
|
|
{
|
|
ok: true,
|
|
outcome: result.kind,
|
|
id: result.document.id,
|
|
version: result.document.version,
|
|
status: result.document.status,
|
|
},
|
|
null,
|
|
2,
|
|
),
|
|
);
|
|
});
|
|
|
|
prdy
|
|
.command('export')
|
|
.description('Render a PRD to a labeled generated-view Markdown file')
|
|
.requiredOption('--project <path>', 'Project path')
|
|
.option('--id <id>', 'PRD document id')
|
|
.option('--out <path>', 'Output path (default docs/prdy/<id>.md)')
|
|
.action(async (options: ExportCommandOptions) => {
|
|
const result = await serviceFor(options.project).exportMarkdown({
|
|
id: options.id,
|
|
outPath: options.out,
|
|
});
|
|
console.log(JSON.stringify({ ok: true, filePath: result.filePath }, null, 2));
|
|
});
|
|
|
|
return program;
|
|
}
|
|
|
|
export async function runPrdyCli(argv: readonly string[] = process.argv): Promise<void> {
|
|
const program = buildPrdyCli();
|
|
await program.parseAsync(argv);
|
|
}
|