Files
stack/packages/mosaic/src/commands/prdy.ts
T
fargoandjarvis d92de53399
ci/woodpecker/push/publish Pipeline was canceled
feat(prd): one transitional PRD authority — RI-4-001 (#1275) (#1294)
Co-authored-by: fargo <[email protected]>
2026-08-18 05:56:51 +00:00

115 lines
4.4 KiB
TypeScript

import type { Command } from 'commander';
import { withAuth } from './with-auth.js';
import { fetchProjects } from '../tui/gateway-api.js';
/**
* `mosaic prdy` — thin adapter over PrdService (@mosaicstack/prdy).
* All reads/writes go through the service; there is no local writer path.
*/
export function registerPrdyCommand(program: Command) {
const cmd = program
.command('prdy')
.description('PRD wizard — create and manage Product Requirement Documents')
.option('-g, --gateway <url>', 'Gateway URL', 'http://localhost:14242')
.option('--init [name]', 'Create a new PRD')
.option('--update [name]', 'Update an existing PRD')
.option('--import <file>', 'Import a YAML PRD document (validated, conflict-aware)')
.option('--accept-successor', 'With --import: accept a conflicted import as next version')
.option('--export [id]', 'Export a PRD as a labeled generated-view Markdown file')
.option('--project <idOrName>', 'Scope to project')
.action(
async (opts: {
gateway: string;
init?: string | boolean;
update?: string | boolean;
import?: string;
acceptSuccessor?: boolean;
export?: string | boolean;
project?: string;
}) => {
// Detect project context when --project flag is provided
if (opts.project) {
try {
const auth = await withAuth(opts.gateway);
const projects = await fetchProjects(auth.gateway, auth.cookie);
const match = projects.find((p) => p.id === opts.project || p.name === opts.project);
if (match) {
console.log(`Project context: ${match.name} (${match.id})\n`);
}
} catch {
// Gateway not available — proceed without project context
}
}
const { PrdService, runPrdWizard } = await import('@mosaicstack/prdy');
const service = new PrdService({ projectPath: process.cwd() });
try {
if (opts.import !== undefined) {
const input = { filePath: opts.import };
if (opts.acceptSuccessor) {
const successor = await service.acceptSuccessor(input);
console.log(
`Import accepted as successor: ${successor.id} v${successor.version} (status: ${successor.status})`,
);
return;
}
const result = await service.importDocument(input);
console.log(
result.kind === 'created'
? `Imported PRD ${result.document.id} v${result.document.version} (status: ${result.document.status})`
: `PRD ${result.document.id} already present with identical content — nothing to do.`,
);
return;
}
if (opts.export !== undefined) {
const id =
typeof opts.export === 'string' && opts.export.length > 0 ? opts.export : undefined;
const result = await service.exportMarkdown({ id });
console.log(
`Generated view written: ${result.filePath} (source authority: YAML under docs/prdy/ — do not edit the Markdown)`,
);
return;
}
const name =
typeof opts.init === 'string'
? opts.init
: typeof opts.update === 'string'
? opts.update
: 'untitled';
if (process.stdout.isTTY) {
await runPrdWizard({
name,
projectPath: process.cwd(),
interactive: true,
});
return;
}
// Non-interactive fallback routes through the service directly.
const doc = await service.create({ name });
console.log(`PRD created: ${doc.id} v${doc.version} (status: ${doc.status})`);
} catch (err) {
if (err instanceof Error && err.name === 'PrdImportConflictError') {
const conflict = err as { proposal?: { version?: number } };
console.error(`${err.message}`);
console.error(
`Original PRD left untouched. To accept the proposed successor (v${conflict.proposal?.version}), re-run with --accept-successor.`,
);
process.exit(1);
}
console.error(`PRD wizard failed: ${err instanceof Error ? err.message : String(err)}`);
process.exit(1);
}
},
);
return cmd;
}