| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225 |
- import { Command } from "commander";
- import { StateManager } from "@actalk/inkos-core";
- import { findProjectRoot, resolveBookId, log, logError } from "../utils.js";
- export const reviewCommand = new Command("review")
- .description("Review and approve chapters");
- reviewCommand
- .command("list")
- .description("List chapters pending review")
- .argument("[book-id]", "Book ID (optional, lists all books if omitted)")
- .option("--json", "Output JSON")
- .action(async (bookId: string | undefined, opts) => {
- try {
- const root = findProjectRoot();
- const state = new StateManager(root);
- const bookIds = bookId ? [bookId] : await state.listBooks();
- const allPending: Array<{
- readonly bookId: string;
- readonly title: string;
- readonly chapter: number;
- readonly chapterTitle: string;
- readonly wordCount: number;
- readonly status: string;
- readonly issues: ReadonlyArray<string>;
- }> = [];
- for (const id of bookIds) {
- const index = await state.loadChapterIndex(id);
- const pending = index.filter(
- (ch) =>
- ch.status === "ready-for-review" || ch.status === "audit-failed",
- );
- if (pending.length === 0) continue;
- const book = await state.loadBookConfig(id);
- if (!opts.json) {
- log(`\n${book.title} (${id}):`);
- }
- for (const ch of pending) {
- allPending.push({
- bookId: id,
- title: book.title,
- chapter: ch.number,
- chapterTitle: ch.title,
- wordCount: ch.wordCount,
- status: ch.status,
- issues: ch.auditIssues,
- });
- if (!opts.json) {
- log(
- ` Ch.${ch.number} "${ch.title}" | ${ch.wordCount}字 | ${ch.status}`,
- );
- if (ch.auditIssues.length > 0) {
- for (const issue of ch.auditIssues) {
- log(` - ${issue}`);
- }
- }
- }
- }
- }
- if (opts.json) {
- log(JSON.stringify({ pending: allPending }, null, 2));
- } else if (allPending.length === 0) {
- log("No chapters pending review.");
- }
- } catch (e) {
- if (opts.json) {
- log(JSON.stringify({ error: String(e) }));
- } else {
- logError(`Failed to list reviews: ${e}`);
- }
- process.exit(1);
- }
- });
- /**
- * Parse "[book-id] <chapter>" style arguments from variadic args.
- * Supports: "3" (auto-detect book) or "my-book 3"
- */
- function parseBookAndChapter(
- args: ReadonlyArray<string>,
- ): { readonly bookIdArg: string | undefined; readonly chapterNum: number } {
- if (args.length === 1) {
- const num = parseInt(args[0]!, 10);
- if (isNaN(num)) {
- throw new Error(`Expected chapter number, got "${args[0]}"`);
- }
- return { bookIdArg: undefined, chapterNum: num };
- }
- if (args.length === 2) {
- const num = parseInt(args[1]!, 10);
- if (isNaN(num)) {
- throw new Error(`Expected chapter number as second argument, got "${args[1]}"`);
- }
- return { bookIdArg: args[0], chapterNum: num };
- }
- throw new Error("Usage: inkos review approve [book-id] <chapter>");
- }
- reviewCommand
- .command("approve")
- .description("Approve a chapter: approve [book-id] <chapter>")
- .argument("<args...>", "Book ID (optional) and chapter number")
- .option("--json", "Output JSON")
- .action(async (args: ReadonlyArray<string>, opts) => {
- try {
- const root = findProjectRoot();
- const { bookIdArg, chapterNum } = parseBookAndChapter(args);
- const bookId = await resolveBookId(bookIdArg, root);
- const state = new StateManager(root);
- const index = [...(await state.loadChapterIndex(bookId))];
- const idx = index.findIndex((ch) => ch.number === chapterNum);
- if (idx === -1) {
- throw new Error(`Chapter ${chapterNum} not found in "${bookId}"`);
- }
- index[idx] = {
- ...index[idx]!,
- status: "approved",
- updatedAt: new Date().toISOString(),
- };
- await state.saveChapterIndex(bookId, index);
- if (opts.json) {
- log(JSON.stringify({ bookId, chapter: chapterNum, status: "approved" }));
- } else {
- log(`Chapter ${chapterNum} approved.`);
- }
- } catch (e) {
- if (opts.json) {
- log(JSON.stringify({ error: String(e) }));
- } else {
- logError(`Failed to approve: ${e}`);
- }
- process.exit(1);
- }
- });
- reviewCommand
- .command("approve-all")
- .description("Approve all pending chapters for a book")
- .argument("[book-id]", "Book ID (auto-detected if only one book)")
- .option("--json", "Output JSON")
- .action(async (bookIdArg: string | undefined, opts) => {
- try {
- const root = findProjectRoot();
- const bookId = await resolveBookId(bookIdArg, root);
- const state = new StateManager(root);
- const index = [...(await state.loadChapterIndex(bookId))];
- let count = 0;
- const now = new Date().toISOString();
- const updated = index.map((ch) => {
- if (ch.status === "ready-for-review" || ch.status === "audit-failed") {
- count++;
- return { ...ch, status: "approved" as const, updatedAt: now };
- }
- return ch;
- });
- await state.saveChapterIndex(bookId, updated);
- if (opts.json) {
- log(JSON.stringify({ bookId, approvedCount: count }));
- } else {
- log(`${count} chapter(s) approved.`);
- }
- } catch (e) {
- if (opts.json) {
- log(JSON.stringify({ error: String(e) }));
- } else {
- logError(`Failed to approve: ${e}`);
- }
- process.exit(1);
- }
- });
- reviewCommand
- .command("reject")
- .description("Reject a chapter: reject [book-id] <chapter>")
- .argument("<args...>", "Book ID (optional) and chapter number")
- .option("--reason <reason>", "Rejection reason")
- .option("--json", "Output JSON")
- .action(async (args: ReadonlyArray<string>, opts) => {
- try {
- const root = findProjectRoot();
- const { bookIdArg, chapterNum } = parseBookAndChapter(args);
- const bookId = await resolveBookId(bookIdArg, root);
- const state = new StateManager(root);
- const index = [...(await state.loadChapterIndex(bookId))];
- const idx = index.findIndex((ch) => ch.number === chapterNum);
- if (idx === -1) {
- throw new Error(`Chapter ${chapterNum} not found in "${bookId}"`);
- }
- index[idx] = {
- ...index[idx]!,
- status: "rejected",
- reviewNote: opts.reason ?? "Rejected without reason",
- updatedAt: new Date().toISOString(),
- };
- await state.saveChapterIndex(bookId, index);
- if (opts.json) {
- log(JSON.stringify({ bookId, chapter: chapterNum, status: "rejected" }));
- } else {
- log(`Chapter ${chapterNum} rejected.`);
- }
- } catch (e) {
- if (opts.json) {
- log(JSON.stringify({ error: String(e) }));
- } else {
- logError(`Failed to reject: ${e}`);
- }
- process.exit(1);
- }
- });
|