import { Command } from "commander"; import { PipelineRunner, StateManager } from "@actalk/inkos-core"; import { readdir, unlink } from "node:fs/promises"; import { join } from "node:path"; import { createInterface } from "node:readline"; import { loadConfig, buildPipelineConfig, findProjectRoot, getLegacyMigrationHint, resolveContext, resolveBookId, log, logError } from "../utils.js"; import { formatWriteNextComplete, formatWriteNextProgress, formatWriteNextResultLines, resolveCliLanguage } from "../localization.js"; export const writeCommand = new Command("write") .description("Write chapters"); writeCommand .command("next") .description("Write the next chapter for a book") .argument("[book-id]", "Book ID (auto-detected if only one book)") .option("--count ", "Number of chapters to write", "1") .option("--words ", "Words per chapter (overrides book config)") .option("--context ", "Creative guidance (natural language)") .option("--context-file ", "Read guidance from file") .option("--json", "Output JSON") .option("-q, --quiet", "Suppress console output") .action(async (bookIdArg: string | undefined, opts) => { try { const root = findProjectRoot(); const bookId = await resolveBookId(bookIdArg, root); const context = await resolveContext(opts); const state = new StateManager(root); const book = await state.loadBookConfig(bookId); const language = resolveCliLanguage(book.language); const migrationHint = await getLegacyMigrationHint(root, bookId); if (migrationHint && !opts.json) { log(`[migration] ${migrationHint}`); } const config = await loadConfig(); const pipeline = new PipelineRunner(buildPipelineConfig(config, root, { externalContext: context, quiet: opts.quiet })); const count = parseInt(opts.count, 10); const wordCount = opts.words ? parseInt(opts.words, 10) : undefined; const results = []; for (let i = 0; i < count; i++) { if (!opts.json) log(formatWriteNextProgress(language, i + 1, count, bookId)); const result = await pipeline.writeNextChapter(bookId, wordCount); results.push(result); if (!opts.json) { for (const line of formatWriteNextResultLines(language, { chapterNumber: result.chapterNumber, title: result.title, wordCount: result.wordCount, auditPassed: result.auditResult.passed, revised: result.revised, status: result.status, issues: result.auditResult.issues, })) { log(line); } log(""); } } if (opts.json) { log(JSON.stringify(results, null, 2)); } else { log(formatWriteNextComplete(language)); } } catch (e) { if (opts.json) { log(JSON.stringify({ error: String(e) })); } else { logError(`Failed to write chapter: ${e}`); } process.exit(1); } }); writeCommand .command("rewrite") .description("Re-generate a specific chapter: rewrite [book-id] ") .argument("", "Book ID (optional) and chapter number") .option("--force", "Skip confirmation prompt") .option("--words ", "Words per chapter (overrides book config)") .option("--json", "Output JSON") .action(async (args: ReadonlyArray, opts) => { try { const root = findProjectRoot(); let bookId: string; let chapter: number; if (args.length === 1) { chapter = parseInt(args[0]!, 10); if (isNaN(chapter)) throw new Error(`Expected chapter number, got "${args[0]}"`); bookId = await resolveBookId(undefined, root); } else if (args.length === 2) { chapter = parseInt(args[1]!, 10); if (isNaN(chapter)) throw new Error(`Expected chapter number, got "${args[1]}"`); bookId = await resolveBookId(args[0], root); } else { throw new Error("Usage: inkos write rewrite [book-id] "); } if (!opts.force) { const rl = createInterface({ input: process.stdin, output: process.stdout }); const answer = await new Promise((resolve) => { rl.question(`Rewrite chapter ${chapter} of "${bookId}"? This will delete chapter ${chapter} and all later chapters. (y/N) `, resolve); }); rl.close(); if (answer.toLowerCase() !== "y") { log("Cancelled."); return; } } const state = new StateManager(root); const bookDir = state.bookDir(bookId); const chaptersDir = join(bookDir, "chapters"); const migrationHint = await getLegacyMigrationHint(root, bookId); if (migrationHint && !opts.json) { log(`[migration] ${migrationHint}`); } // Remove existing chapter file const files = await readdir(chaptersDir); const paddedNum = String(chapter).padStart(4, "0"); const existing = files.filter((f) => f.startsWith(paddedNum) && f.endsWith(".md")); for (const f of existing) { await unlink(join(chaptersDir, f)); if (!opts.json) log(`Removed: ${f}`); } // Remove from index (and all chapters after it) const index = await state.loadChapterIndex(bookId); const trimmed = index.filter((ch) => ch.number < chapter); await state.saveChapterIndex(bookId, trimmed); // Also remove later chapter files since state will be rolled back const laterFiles = files.filter((f) => { const num = parseInt(f.slice(0, 4), 10); return num > chapter && f.endsWith(".md"); }); for (const f of laterFiles) { await unlink(join(chaptersDir, f)); if (!opts.json) log(`Removed later chapter: ${f}`); } // Restore state to previous chapter's end-state (chapter 1 uses snapshot-0 from initBook) const restoreFrom = chapter - 1; const restored = await state.restoreState(bookId, restoreFrom); if (restored) { if (!opts.json) log(`State restored from chapter ${restoreFrom} snapshot.`); } else { if (!opts.json) log(`Warning: no snapshot for chapter ${restoreFrom}. Using current state.`); } if (!opts.json) log(`Regenerating chapter ${chapter}...`); const wordCount = opts.words ? parseInt(opts.words, 10) : undefined; const config = await loadConfig(); const pipeline = new PipelineRunner(buildPipelineConfig(config, root)); const result = await pipeline.writeNextChapter(bookId, wordCount); const book = await state.loadBookConfig(bookId); const language = resolveCliLanguage(book.language); if (opts.json) { log(JSON.stringify(result, null, 2)); } else { for (const line of formatWriteNextResultLines(language, { chapterNumber: result.chapterNumber, title: result.title, wordCount: result.wordCount, auditPassed: result.auditResult.passed, revised: result.revised, status: result.status, issues: result.auditResult.issues, })) { log(line); } } } catch (e) { if (opts.json) { log(JSON.stringify({ error: String(e) })); } else { logError(`Failed to rewrite chapter: ${e}`); } process.exit(1); } });