import { AuthoredSyncError, syncAuthoredTweets, type AuthoredSyncMode, } from "#/lib/authored-live"; import { syncFollowGraph } from "#/lib/follow-graph"; import { syncMentionThreads } from "#/lib/mention-threads-live"; import { syncMentions } from "#/lib/mentions-live"; import { syncTimelineCollection, type TimelineCollectionMode, } from "#/lib/timeline-collections-live"; import { syncHomeTimeline } from "#/lib/timeline-live"; import { syncXLists, type XListSyncMode } from "#/lib/x-lists"; import { errorMessage, type CliCommandContext } from "./command-context"; export function registerSyncCommands({ program, print, autoSyncAfterWrite, }: CliCommandContext) { const syncCommand = program .command("sync") .description("Refresh live Twitter collections into the local store"); syncCommand .command("lists") .description("Sync owned X Lists and rate-limited membership pages") .option("--account ", "Account username or id") .option("--mode ", "auto, bird, or xurl", "auto") .option("--max-lists ", "Maximum owned Lists to inspect", "20") .option("--member-limit ", "Members per transport page", "20") .option("--max-member-pages ", "Maximum membership pages per List", "1") .option("--delay-ms ", "Delay between Lists", "1000") .action(async (options) => { try { const result = await syncXLists({ account: options.account, mode: options.mode as XListSyncMode, maxLists: Number(options.maxLists), memberLimit: Number(options.memberLimit), maxMemberPages: Number(options.maxMemberPages), delayMs: Number(options.delayMs), }); await autoSyncAfterWrite(); print(result, true); if (!result.ok) process.exitCode = 5; } catch (error) { print( { ok: false, kind: "lists", mode: options.mode, error: errorMessage(error), }, true, ); process.exitCode = 1; } }); syncCommand .command("timeline") .description("Refresh live home timeline through xurl or bird") .option("--account ", "Account username or id") .option("--mode ", "auto, xurl, or bird", "auto") .option("--limit ", "Result limit", "100") .option("--max-pages ", "Stop after N xurl pages", "3") .option("--for-you", 'Fetch "For You" instead of chronological Following') .option("--cache-ttl ", "Live-cache freshness window", "120") .option("--refresh", "Bypass live-cache freshness window") .action(async (options) => { const result = await syncHomeTimeline({ account: options.account, mode: options.mode, limit: Number(options.limit), maxPages: Number(options.maxPages), following: !options.forYou, refresh: Boolean(options.refresh), cacheTtlMs: Number(options.cacheTtl) * 1000, }); await autoSyncAfterWrite(); print(result, true); }); syncCommand .command("mentions") .description("Refresh live mentions through xurl or bird") .option("--account ", "Account username or id") .option("--mode ", "auto, bird, or xurl", "auto") .option("--limit ", "Result limit per page", "20") .option("--max-pages ", "Stop after N pages") .option("--since-id ", "Fetch mentions newer than this tweet id") .option( "--start-time ", "Fetch mentions created at or after this time", ) .option("--refresh", "Bypass live-cache freshness window") .option("--cache-ttl ", "Live-cache freshness window", "120") .action(async (options) => { try { const result = await syncMentions({ account: options.account, mode: options.mode, limit: Number(options.limit), maxPages: options.maxPages ? Number(options.maxPages) : undefined, sinceId: options.sinceId, startTime: options.startTime, refresh: Boolean(options.refresh), cacheTtlMs: Number(options.cacheTtl) * 1000, }); await autoSyncAfterWrite(); print(result, true); if (result.partial) process.exitCode = 5; } catch (error) { print( { ok: false, kind: "mentions", mode: options.mode ?? "xurl", error: errorMessage(error), }, true, ); process.exitCode = 1; } }); syncCommand .command("authored") .description("Refresh authenticated authored tweets through xurl") .option("--account ", "Account username or id") .option("--mode ", "xurl", "xurl") .option("--limit ", "X API page size", "100") .option("--max-pages ", "Stop after N pages and resume later") .option("--since-id ", "Override the stored since_id cursor") .option( "--until-id ", "Fetch tweets older than this id without moving the cursor", ) .action(async (options) => { try { const result = await syncAuthoredTweets({ account: options.account, mode: options.mode as AuthoredSyncMode, limit: Number(options.limit), maxPages: options.maxPages ? Number(options.maxPages) : undefined, sinceId: options.sinceId, untilId: options.untilId, }); await autoSyncAfterWrite(); print(result, true); if (result.partial) process.exitCode = 5; } catch (error) { print( { ok: false, kind: "authored", source: "xurl", error: errorMessage(error), }, true, ); process.exitCode = error instanceof AuthoredSyncError ? error.exitCode : 1; } }); syncCommand .command("mention-threads") .description( "Fetch tweet conversation context for recent mentions through bird or xurl", ) .option("--account ", "Account username or id") .option("--mode ", "bird or xurl", "bird") .option("--limit ", "Recent mentions to inspect", "30") .option("--delay-ms ", "Delay between thread fetches", "1500") .option("--timeout-ms ", "Per-thread timeout", "15000") .option("--all", "Fetch all retrievable thread pages") .option("--max-pages ", "Stop after N pages") .action(async (options) => { try { const result = await syncMentionThreads({ account: options.account, mode: options.mode, limit: Number(options.limit), delayMs: Number(options.delayMs), timeoutMs: Number(options.timeoutMs), all: Boolean(options.all), maxPages: options.maxPages ? Number(options.maxPages) : undefined, }); await autoSyncAfterWrite(); print(result, true); if (result.partial) process.exitCode = 5; } catch (error) { print( { ok: false, kind: "mention-threads", mode: options.mode ?? "bird", error: errorMessage(error), }, true, ); process.exitCode = 1; } }); for (const kind of ["likes", "bookmarks"] as const) { syncCommand .command(kind) .description(`Refresh live ${kind} through xurl or bird`) .option("--account ", "Account username or id") .option("--mode ", "auto, xurl, or bird", "auto") .option("--limit ", "Per-page/result limit", "20") .option("--all", "Fetch every retrievable page") .option( "--max-pages ", "Stop after N pages when using --all or --early-stop", ) .option("--early-stop", "Stop when a fetched page is already fully local") .option("--cache-ttl ", "Live-cache freshness window", "120") .option("--refresh", "Bypass live-cache freshness window") .action(async (options) => { const result = await syncTimelineCollection({ kind, account: options.account, mode: options.mode as TimelineCollectionMode, limit: Number(options.limit), all: Boolean(options.all) || options.maxPages !== undefined, maxPages: options.maxPages ? Number(options.maxPages) : undefined, refresh: Boolean(options.refresh), cacheTtlMs: Number(options.cacheTtl) * 1000, earlyStop: Boolean(options.earlyStop), }); await autoSyncAfterWrite(); print(result, true); }); } for (const direction of ["followers", "following"] as const) { syncCommand .command(direction) .description( `Dry-run or refresh live ${direction} into the local follow graph`, ) .option("--account ", "Account username or id") .option("--mode ", "auto, bird, or xurl", "auto") .option("--limit ", "X API users per page", "1000") .option("--max-pages ", "Stop after N pages") .option("--max-resources ", "Stop after N unique users") .option("--cache-ttl ", "Live-cache freshness window", "86400") .option("--refresh", "Bypass the live-cache freshness window") .option("--allow-partial", "Acknowledge capped/incomplete snapshot") .option("--yes", "Confirm live sync or fresh-cache merge") .action(async (options) => { try { const result = await syncFollowGraph({ direction, account: options.account, mode: options.mode, limit: Number(options.limit), maxPages: options.maxPages ? Number(options.maxPages) : undefined, maxResources: options.maxResources ? Number(options.maxResources) : undefined, cacheTtlMs: Number(options.cacheTtl) * 1000, refresh: Boolean(options.refresh), allowPartial: Boolean(options.allowPartial), yes: Boolean(options.yes), }); if (!result.dryRun) await autoSyncAfterWrite(); print(result, true); } catch (error) { print({ ok: false, direction, error: errorMessage(error) }, true); process.exitCode = 1; } }); } }