status.ts 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138
  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 degraded = index.filter(
  41. (ch) => ch.status === "state-degraded",
  42. ).length;
  43. const totalWords = index.reduce((sum, ch) => sum + ch.wordCount, 0);
  44. const avgWords = index.length > 0 ? Math.round(totalWords / index.length) : 0;
  45. booksData.push({
  46. id,
  47. title: book.title,
  48. status: book.status,
  49. genre: book.genre,
  50. platform: book.platform,
  51. chapters: persistedChapterCount,
  52. targetChapters: book.targetChapters,
  53. totalWords,
  54. avgWordsPerChapter: avgWords,
  55. approved,
  56. pending,
  57. failed,
  58. degraded,
  59. ...(migrationHint ? { migrationHint } : {}),
  60. ...(opts.chapters ? {
  61. chapterList: index.map((ch) => ({
  62. number: ch.number,
  63. title: ch.title,
  64. status: ch.status,
  65. wordCount: ch.wordCount,
  66. ...(ch.status === "audit-failed" || ch.status === "state-degraded"
  67. ? { issues: ch.auditIssues }
  68. : {}),
  69. })),
  70. } : {}),
  71. });
  72. if (!opts.json) {
  73. log(` ${book.title} (${id})`);
  74. log(` Status: ${book.status}`);
  75. log(` Platform: ${book.platform} | Genre: ${book.genre}`);
  76. log(` Chapters: ${persistedChapterCount} / ${book.targetChapters}`);
  77. log(` Words: ${totalWords.toLocaleString()} (avg ${avgWords}/ch)`);
  78. log(` Approved: ${approved} | Pending: ${pending} | Failed: ${failed} | Degraded: ${degraded}`);
  79. if (migrationHint) {
  80. log(` Migration: ${migrationHint}`);
  81. }
  82. if (opts.chapters && index.length > 0) {
  83. log("");
  84. for (const ch of index) {
  85. const icon = ch.status === "approved"
  86. ? "+"
  87. : ch.status === "audit-failed"
  88. ? "!"
  89. : ch.status === "state-degraded"
  90. ? "x"
  91. : "~";
  92. log(` [${icon}] Ch.${ch.number} "${ch.title}" | ${formatLengthCount(ch.wordCount, countingMode)} | ${ch.status}`);
  93. if ((ch.status === "audit-failed" || ch.status === "state-degraded") && ch.auditIssues.length > 0) {
  94. const criticals = ch.auditIssues.filter((i: string) => i.startsWith("[critical]"));
  95. const warnings = ch.auditIssues.filter((i: string) => i.startsWith("[warning]"));
  96. if (criticals.length > 0) {
  97. for (const issue of criticals) {
  98. log(` ${issue}`);
  99. }
  100. }
  101. if (warnings.length > 0) {
  102. if (ch.status === "state-degraded") {
  103. for (const issue of warnings) {
  104. log(` ${issue}`);
  105. }
  106. } else {
  107. log(` + ${warnings.length} warning(s)`);
  108. }
  109. }
  110. }
  111. }
  112. }
  113. log("");
  114. }
  115. }
  116. if (opts.json) {
  117. log(JSON.stringify({ project: root, books: booksData }, null, 2));
  118. }
  119. } catch (e) {
  120. if (opts.json) {
  121. log(JSON.stringify({ error: String(e) }));
  122. } else {
  123. logError(`Failed to get status: ${e}`);
  124. }
  125. process.exit(1);
  126. }
  127. });