280 lines · 9.2 KB · TypeScript
RawDownload
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 <username>", "Account username or id")		.option("--mode <mode>", "auto, bird, or xurl", "auto")		.option("--max-lists <n>", "Maximum owned Lists to inspect", "20")		.option("--member-limit <n>", "Members per transport page", "20")		.option("--max-member-pages <n>", "Maximum membership pages per List", "1")		.option("--delay-ms <n>", "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 <username>", "Account username or id")		.option("--mode <mode>", "auto, xurl, or bird", "auto")		.option("--limit <n>", "Result limit", "100")		.option("--max-pages <n>", "Stop after N xurl pages", "3")		.option("--for-you", 'Fetch "For You" instead of chronological Following')		.option("--cache-ttl <seconds>", "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 <username>", "Account username or id")		.option("--mode <mode>", "auto, bird, or xurl", "auto")		.option("--limit <n>", "Result limit per page", "20")		.option("--max-pages <n>", "Stop after N pages")		.option("--since-id <id>", "Fetch mentions newer than this tweet id")		.option(			"--start-time <iso>",			"Fetch mentions created at or after this time",		)		.option("--refresh", "Bypass live-cache freshness window")		.option("--cache-ttl <seconds>", "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 <username>", "Account username or id")		.option("--mode <mode>", "xurl", "xurl")		.option("--limit <n>", "X API page size", "100")		.option("--max-pages <n>", "Stop after N pages and resume later")		.option("--since-id <tweetId>", "Override the stored since_id cursor")		.option(			"--until-id <tweetId>",			"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 <username>", "Account username or id")		.option("--mode <mode>", "bird or xurl", "bird")		.option("--limit <n>", "Recent mentions to inspect", "30")		.option("--delay-ms <n>", "Delay between thread fetches", "1500")		.option("--timeout-ms <n>", "Per-thread timeout", "15000")		.option("--all", "Fetch all retrievable thread pages")		.option("--max-pages <n>", "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 <username>", "Account username or id")			.option("--mode <mode>", "auto, xurl, or bird", "auto")			.option("--limit <n>", "Per-page/result limit", "20")			.option("--all", "Fetch every retrievable page")			.option(				"--max-pages <n>",				"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 <seconds>", "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 <username>", "Account username or id")			.option("--mode <mode>", "auto, bird, or xurl", "auto")			.option("--limit <n>", "X API users per page", "1000")			.option("--max-pages <n>", "Stop after N pages")			.option("--max-resources <n>", "Stop after N unique users")			.option("--cache-ttl <seconds>", "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;				}			});	}}