|
|
@@ -1,6 +1,6 @@
|
|
|
import { Command } from "commander";
|
|
|
import { StateManager } from "@actalk/inkos-core";
|
|
|
-import { findProjectRoot, log, logError } from "../utils.js";
|
|
|
+import { findProjectRoot, resolveBookId, log, logError } from "../utils.js";
|
|
|
|
|
|
export const reviewCommand = new Command("review")
|
|
|
.description("Review and approve chapters");
|
|
|
@@ -9,12 +9,22 @@ reviewCommand
|
|
|
.command("list")
|
|
|
.description("List chapters pending review")
|
|
|
.argument("[book-id]", "Book ID (optional, lists all books if omitted)")
|
|
|
- .action(async (bookId?: string) => {
|
|
|
+ .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);
|
|
|
@@ -26,20 +36,44 @@ reviewCommand
|
|
|
if (pending.length === 0) continue;
|
|
|
|
|
|
const book = await state.loadBookConfig(id);
|
|
|
- log(`\n${book.title} (${id}):`);
|
|
|
+
|
|
|
+ if (!opts.json) {
|
|
|
+ log(`\n${book.title} (${id}):`);
|
|
|
+ }
|
|
|
for (const ch of pending) {
|
|
|
- log(
|
|
|
- ` Ch.${ch.number} "${ch.title}" | ${ch.wordCount}字 | ${ch.status}`,
|
|
|
- );
|
|
|
- if (ch.auditIssues.length > 0) {
|
|
|
- for (const issue of ch.auditIssues) {
|
|
|
- log(` - ${issue}`);
|
|
|
+ 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) {
|
|
|
- logError(`Failed to list reviews: ${e}`);
|
|
|
+ if (opts.json) {
|
|
|
+ log(JSON.stringify({ error: String(e) }));
|
|
|
+ } else {
|
|
|
+ logError(`Failed to list reviews: ${e}`);
|
|
|
+ }
|
|
|
process.exit(1);
|
|
|
}
|
|
|
});
|
|
|
@@ -47,18 +81,33 @@ reviewCommand
|
|
|
reviewCommand
|
|
|
.command("approve")
|
|
|
.description("Approve a chapter")
|
|
|
- .argument("<book-id>", "Book ID")
|
|
|
+ .argument("[book-id]", "Book ID (auto-detected if only one book)")
|
|
|
.argument("<chapter>", "Chapter number")
|
|
|
- .action(async (bookId: string, chapterStr: string) => {
|
|
|
+ .option("--json", "Output JSON")
|
|
|
+ .action(async (bookIdArg: string, chapterStr: string, opts) => {
|
|
|
try {
|
|
|
const root = findProjectRoot();
|
|
|
- const state = new StateManager(root);
|
|
|
- const chapterNum = parseInt(chapterStr, 10);
|
|
|
|
|
|
+ let bookId: string;
|
|
|
+ let chapterNum: number;
|
|
|
+ if (!chapterStr || isNaN(parseInt(chapterStr, 10))) {
|
|
|
+ bookId = await resolveBookId(undefined, root);
|
|
|
+ chapterNum = parseInt(bookIdArg, 10);
|
|
|
+ } else {
|
|
|
+ bookId = await resolveBookId(bookIdArg, root);
|
|
|
+ chapterNum = parseInt(chapterStr, 10);
|
|
|
+ }
|
|
|
+
|
|
|
+ const state = new StateManager(root);
|
|
|
const index = [...(await state.loadChapterIndex(bookId))];
|
|
|
const idx = index.findIndex((ch) => ch.number === chapterNum);
|
|
|
if (idx === -1) {
|
|
|
- logError(`Chapter ${chapterNum} not found`);
|
|
|
+ const msg = `Chapter ${chapterNum} not found in "${bookId}"`;
|
|
|
+ if (opts.json) {
|
|
|
+ log(JSON.stringify({ error: msg }));
|
|
|
+ } else {
|
|
|
+ logError(msg);
|
|
|
+ }
|
|
|
process.exit(1);
|
|
|
}
|
|
|
|
|
|
@@ -68,9 +117,18 @@ reviewCommand
|
|
|
updatedAt: new Date().toISOString(),
|
|
|
};
|
|
|
await state.saveChapterIndex(bookId, index);
|
|
|
- log(`Chapter ${chapterNum} approved.`);
|
|
|
+
|
|
|
+ if (opts.json) {
|
|
|
+ log(JSON.stringify({ bookId, chapter: chapterNum, status: "approved" }));
|
|
|
+ } else {
|
|
|
+ log(`Chapter ${chapterNum} approved.`);
|
|
|
+ }
|
|
|
} catch (e) {
|
|
|
- logError(`Failed to approve: ${e}`);
|
|
|
+ if (opts.json) {
|
|
|
+ log(JSON.stringify({ error: String(e) }));
|
|
|
+ } else {
|
|
|
+ logError(`Failed to approve: ${e}`);
|
|
|
+ }
|
|
|
process.exit(1);
|
|
|
}
|
|
|
});
|
|
|
@@ -78,10 +136,12 @@ reviewCommand
|
|
|
reviewCommand
|
|
|
.command("approve-all")
|
|
|
.description("Approve all pending chapters for a book")
|
|
|
- .argument("<book-id>", "Book ID")
|
|
|
- .action(async (bookId: string) => {
|
|
|
+ .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))];
|
|
|
@@ -97,9 +157,18 @@ reviewCommand
|
|
|
});
|
|
|
|
|
|
await state.saveChapterIndex(bookId, updated);
|
|
|
- log(`${count} chapter(s) approved.`);
|
|
|
+
|
|
|
+ if (opts.json) {
|
|
|
+ log(JSON.stringify({ bookId, approvedCount: count }));
|
|
|
+ } else {
|
|
|
+ log(`${count} chapter(s) approved.`);
|
|
|
+ }
|
|
|
} catch (e) {
|
|
|
- logError(`Failed to approve: ${e}`);
|
|
|
+ if (opts.json) {
|
|
|
+ log(JSON.stringify({ error: String(e) }));
|
|
|
+ } else {
|
|
|
+ logError(`Failed to approve: ${e}`);
|
|
|
+ }
|
|
|
process.exit(1);
|
|
|
}
|
|
|
});
|
|
|
@@ -107,19 +176,34 @@ reviewCommand
|
|
|
reviewCommand
|
|
|
.command("reject")
|
|
|
.description("Reject a chapter")
|
|
|
- .argument("<book-id>", "Book ID")
|
|
|
+ .argument("[book-id]", "Book ID (auto-detected if only one book)")
|
|
|
.argument("<chapter>", "Chapter number")
|
|
|
.option("--reason <reason>", "Rejection reason")
|
|
|
- .action(async (bookId: string, chapterStr: string, opts) => {
|
|
|
+ .option("--json", "Output JSON")
|
|
|
+ .action(async (bookIdArg: string, chapterStr: string, opts) => {
|
|
|
try {
|
|
|
const root = findProjectRoot();
|
|
|
- const state = new StateManager(root);
|
|
|
- const chapterNum = parseInt(chapterStr, 10);
|
|
|
|
|
|
+ let bookId: string;
|
|
|
+ let chapterNum: number;
|
|
|
+ if (!chapterStr || isNaN(parseInt(chapterStr, 10))) {
|
|
|
+ bookId = await resolveBookId(undefined, root);
|
|
|
+ chapterNum = parseInt(bookIdArg, 10);
|
|
|
+ } else {
|
|
|
+ bookId = await resolveBookId(bookIdArg, root);
|
|
|
+ chapterNum = parseInt(chapterStr, 10);
|
|
|
+ }
|
|
|
+
|
|
|
+ const state = new StateManager(root);
|
|
|
const index = [...(await state.loadChapterIndex(bookId))];
|
|
|
const idx = index.findIndex((ch) => ch.number === chapterNum);
|
|
|
if (idx === -1) {
|
|
|
- logError(`Chapter ${chapterNum} not found`);
|
|
|
+ const msg = `Chapter ${chapterNum} not found in "${bookId}"`;
|
|
|
+ if (opts.json) {
|
|
|
+ log(JSON.stringify({ error: msg }));
|
|
|
+ } else {
|
|
|
+ logError(msg);
|
|
|
+ }
|
|
|
process.exit(1);
|
|
|
}
|
|
|
|
|
|
@@ -130,9 +214,18 @@ reviewCommand
|
|
|
updatedAt: new Date().toISOString(),
|
|
|
};
|
|
|
await state.saveChapterIndex(bookId, index);
|
|
|
- log(`Chapter ${chapterNum} rejected.`);
|
|
|
+
|
|
|
+ if (opts.json) {
|
|
|
+ log(JSON.stringify({ bookId, chapter: chapterNum, status: "rejected" }));
|
|
|
+ } else {
|
|
|
+ log(`Chapter ${chapterNum} rejected.`);
|
|
|
+ }
|
|
|
} catch (e) {
|
|
|
- logError(`Failed to reject: ${e}`);
|
|
|
+ if (opts.json) {
|
|
|
+ log(JSON.stringify({ error: String(e) }));
|
|
|
+ } else {
|
|
|
+ logError(`Failed to reject: ${e}`);
|
|
|
+ }
|
|
|
process.exit(1);
|
|
|
}
|
|
|
});
|