import chalk from "chalk"; import dayjs from "dayjs"; import { define } from "gunshi"; import { openDb } from "@/db"; import * as syncRunsQ from "@/db/queries/sync-runs"; import { whatsNew } from "@/services/whats-new-service"; import { formatIso, parseToMs } from "@/utils/dates"; import { ExitCodes, exitWith, wrap } from "../envelope"; import { enterJsonMode, isJsonMode, printHuman, printJson } from "../output"; export const whatsNewCommand = define({ name: "whats-new", description: "Aggregate new releases, price drops, and new magnets since a cutoff", args: { since: { type: "string", description: "Cutoff (YYYY-MM-DD, ISO, or ms)" }, "since-last-sync": { type: "boolean", description: "Use the last successful sync's finished_at as the cutoff (default)" }, author: { type: "string", short: "a", description: "Restrict results to a single author" }, db: { type: "string", description: "SQLite DB path (default: ~/.fc2cma/db.sqlite or $FC2CMA_DB)" }, json: { type: "boolean", description: "Emit machine-readable JSON envelope on stdout" } }, run(ctx) { const args = ctx.values; enterJsonMode(Boolean(args.json)); if (args.since && args["since-last-sync"]) { return exitWith( ExitCodes.USAGE, "whats-new: --since and --since-last-sync are mutually exclusive" ); } const db = openDb(args.db); const warnings: string[] = []; let sinceMs: number; if (args.since) { try { sinceMs = parseToMs(args.since); } catch (err) { db.close(); const msg = err instanceof Error ? err.message : String(err); return exitWith(ExitCodes.USAGE, `whats-new: ${msg}`); } } else { const lastRun = syncRunsQ.lastSuccessfulRun(db, args.author); if (lastRun && lastRun.finishedAt !== null) { sinceMs = lastRun.finishedAt; } else { sinceMs = dayjs().subtract(7, "day").valueOf(); warnings.push("no successful sync found; defaulting to 7 days ago"); } } const payload = whatsNew({ db, author: args.author ?? null, sinceMs }); if (isJsonMode()) { printJson(wrap("whats-new", payload, warnings)); } else { const sinceIso = formatIso(sinceMs); printHuman(chalk.bold(`since ${sinceIso}${args.author ? ` (author=${args.author})` : ""}`)); printHuman( ` new releases: ${chalk.green(payload.counts.newReleases)} price drops: ${chalk.green(payload.counts.priceDrops)} new magnets: ${chalk.green(payload.counts.newMagnets)}` ); if (payload.newReleases.length > 0) { printHuman(chalk.bold("\n New releases:")); for (const r of payload.newReleases) { printHuman(` ${chalk.cyan(r.id)} ${r.author} ${r.title}`); } } if (payload.priceDrops.length > 0) { printHuman(chalk.bold("\n Price drops:")); for (const d of payload.priceDrops) { const pct = (d.pctDrop * 100).toFixed(0); printHuman( ` ${chalk.cyan(d.id)} ¥${d.from.priceYen} → ¥${d.to.priceYen} ${chalk.red(`(-${pct}%)`)} ${d.title}` ); } } for (const w of warnings) printHuman(chalk.yellow(` warning: ${w}`)); } db.close(); } });