status.ts 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120
  1. import { Command } from "commander";
  2. import { StateManager, formatLengthCount, readGenreProfile, resolveLengthCountingMode } from "@actalk/inkos-core";
  3. import { findProjectRoot, getLegacyMigrationHint, log, logError } from "../utils.js";
  4. export const statusCommand = new Command("status")
  5. .description("Show project status")
  6. .argument("[book-id]", "Book ID (optional, shows all if omitted)")
  7. .option("--chapters", "Show per-chapter status and issues")
  8. .option("--json", "Output JSON")
  9. .action(async (bookIdArg: string | undefined, opts) => {
  10. try {
  11. const root = findProjectRoot();
  12. const state = new StateManager(root);
  13. const allBookIds = await state.listBooks();
  14. const bookIds = bookIdArg ? [bookIdArg] : allBookIds;
  15. if (bookIdArg && !allBookIds.includes(bookIdArg)) {
  16. throw new Error(
  17. `Book "${bookIdArg}" not found. Available: ${allBookIds.join(", ") || "(none)"}`,
  18. );
  19. }
  20. const booksData = [];
  21. if (!opts.json) {
  22. log(`InkOS Project: ${root}`);
  23. log(`Books: ${allBookIds.length}`);
  24. log("");
  25. }
  26. for (const id of bookIds) {
  27. const book = await state.loadBookConfig(id);
  28. const index = await state.loadChapterIndex(id);
  29. const migrationHint = await getLegacyMigrationHint(root, id);
  30. const persistedChapterCount = await state.getPersistedChapterCount(id);
  31. const { profile: genreProfile } = await readGenreProfile(root, book.genre);
  32. const countingMode = resolveLengthCountingMode(book.language ?? genreProfile.language);
  33. const approved = index.filter((ch) => ch.status === "approved").length;
  34. const pending = index.filter(
  35. (ch) => ch.status === "ready-for-review",
  36. ).length;
  37. const failed = index.filter(
  38. (ch) => ch.status === "audit-failed",
  39. ).length;
  40. const totalWords = index.reduce((sum, ch) => sum + ch.wordCount, 0);
  41. const avgWords = index.length > 0 ? Math.round(totalWords / index.length) : 0;
  42. booksData.push({
  43. id,
  44. title: book.title,
  45. status: book.status,
  46. genre: book.genre,
  47. platform: book.platform,
  48. chapters: persistedChapterCount,
  49. targetChapters: book.targetChapters,
  50. totalWords,
  51. avgWordsPerChapter: avgWords,
  52. approved,
  53. pending,
  54. failed,
  55. ...(migrationHint ? { migrationHint } : {}),
  56. ...(opts.chapters ? {
  57. chapterList: index.map((ch) => ({
  58. number: ch.number,
  59. title: ch.title,
  60. status: ch.status,
  61. wordCount: ch.wordCount,
  62. ...(ch.status === "audit-failed" ? { issues: ch.auditIssues } : {}),
  63. })),
  64. } : {}),
  65. });
  66. if (!opts.json) {
  67. log(` ${book.title} (${id})`);
  68. log(` Status: ${book.status}`);
  69. log(` Platform: ${book.platform} | Genre: ${book.genre}`);
  70. log(` Chapters: ${persistedChapterCount} / ${book.targetChapters}`);
  71. log(` Words: ${totalWords.toLocaleString()} (avg ${avgWords}/ch)`);
  72. log(` Approved: ${approved} | Pending: ${pending} | Failed: ${failed}`);
  73. if (migrationHint) {
  74. log(` Migration: ${migrationHint}`);
  75. }
  76. if (opts.chapters && index.length > 0) {
  77. log("");
  78. for (const ch of index) {
  79. const icon = ch.status === "approved" ? "+" : ch.status === "audit-failed" ? "!" : "~";
  80. log(` [${icon}] Ch.${ch.number} "${ch.title}" | ${formatLengthCount(ch.wordCount, countingMode)} | ${ch.status}`);
  81. if (ch.status === "audit-failed" && ch.auditIssues.length > 0) {
  82. const criticals = ch.auditIssues.filter((i: string) => i.startsWith("[critical]"));
  83. const warnings = ch.auditIssues.filter((i: string) => i.startsWith("[warning]"));
  84. if (criticals.length > 0) {
  85. for (const issue of criticals) {
  86. log(` ${issue}`);
  87. }
  88. }
  89. if (warnings.length > 0) {
  90. log(` + ${warnings.length} warning(s)`);
  91. }
  92. }
  93. }
  94. }
  95. log("");
  96. }
  97. }
  98. if (opts.json) {
  99. log(JSON.stringify({ project: root, books: booksData }, null, 2));
  100. }
  101. } catch (e) {
  102. if (opts.json) {
  103. log(JSON.stringify({ error: String(e) }));
  104. } else {
  105. logError(`Failed to get status: ${e}`);
  106. }
  107. process.exit(1);
  108. }
  109. });