refactor(cli): split commands into modules
This commit is contained in:
@@ -0,0 +1,47 @@
|
||||
import type { Command } from 'commander';
|
||||
import type { CliContext } from '../cli/shared.js';
|
||||
import { extractBookmarkFolderId } from '../lib/extract-bookmark-folder-id.js';
|
||||
import { TwitterClient } from '../lib/twitter-client.js';
|
||||
|
||||
export function registerBookmarksCommand(program: Command, ctx: CliContext): void {
|
||||
program
|
||||
.command('bookmarks')
|
||||
.description('Get your bookmarked tweets')
|
||||
.option('-n, --count <number>', 'Number of bookmarks to fetch', '20')
|
||||
.option('--folder-id <id>', 'Bookmark folder (collection) id')
|
||||
.option('--json', 'Output as JSON')
|
||||
.action(async (cmdOpts: { count?: string; json?: boolean; folderId?: string }) => {
|
||||
const opts = program.opts();
|
||||
const timeoutMs = ctx.resolveTimeoutFromOptions(opts);
|
||||
const count = Number.parseInt(cmdOpts.count || '20', 10);
|
||||
|
||||
const { cookies, warnings } = await ctx.resolveCredentialsFromOptions(opts);
|
||||
|
||||
for (const warning of warnings) {
|
||||
console.error(`${ctx.p('warn')}${warning}`);
|
||||
}
|
||||
|
||||
if (!cookies.authToken || !cookies.ct0) {
|
||||
console.error(`${ctx.p('err')}Missing required credentials`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const client = new TwitterClient({ cookies, timeoutMs });
|
||||
const folderId = cmdOpts.folderId ? extractBookmarkFolderId(cmdOpts.folderId) : null;
|
||||
if (cmdOpts.folderId && !folderId) {
|
||||
console.error(`${ctx.p('err')}Invalid --folder-id. Expected numeric ID or https://x.com/i/bookmarks/<id>.`);
|
||||
process.exit(1);
|
||||
}
|
||||
const result = folderId
|
||||
? await client.getBookmarkFolderTimeline(folderId, count)
|
||||
: await client.getBookmarks(count);
|
||||
|
||||
if (result.success && result.tweets) {
|
||||
const emptyMessage = folderId ? 'No bookmarks found in folder.' : 'No bookmarks found.';
|
||||
ctx.printTweets(result.tweets, { json: cmdOpts.json, emptyMessage });
|
||||
} else {
|
||||
console.error(`${ctx.p('err')}Failed to fetch bookmarks: ${result.error}`);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import type { Command } from 'commander';
|
||||
import type { CliContext } from '../cli/shared.js';
|
||||
|
||||
export function registerCheckCommand(program: Command, ctx: CliContext): void {
|
||||
program
|
||||
.command('check')
|
||||
.description('Check credential availability')
|
||||
.action(async () => {
|
||||
const opts = program.opts();
|
||||
const { cookies, warnings } = await ctx.resolveCredentialsFromOptions(opts);
|
||||
|
||||
console.log(`${ctx.p('info')}Credential check`);
|
||||
console.log('─'.repeat(40));
|
||||
|
||||
if (cookies.authToken) {
|
||||
console.log(`${ctx.p('ok')}auth_token: ${cookies.authToken.slice(0, 10)}...`);
|
||||
} else {
|
||||
console.log(`${ctx.p('err')}auth_token: not found`);
|
||||
}
|
||||
|
||||
if (cookies.ct0) {
|
||||
console.log(`${ctx.p('ok')}ct0: ${cookies.ct0.slice(0, 10)}...`);
|
||||
} else {
|
||||
console.log(`${ctx.p('err')}ct0: not found`);
|
||||
}
|
||||
|
||||
if (cookies.source) {
|
||||
console.log(`${ctx.l('source')}${cookies.source}`);
|
||||
}
|
||||
|
||||
if (warnings.length > 0) {
|
||||
console.log(`\n${ctx.p('warn')}Warnings:`);
|
||||
for (const warning of warnings) {
|
||||
console.log(` - ${warning}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (cookies.authToken && cookies.ct0) {
|
||||
console.log(`\n${ctx.p('ok')}Ready to tweet!`);
|
||||
} else {
|
||||
console.log(`\n${ctx.p('err')}Missing credentials. Options:`);
|
||||
console.log(' 1. Login to x.com in Safari/Chrome/Firefox');
|
||||
console.log(' 2. Set AUTH_TOKEN and CT0 environment variables');
|
||||
console.log(' 3. Use --auth-token and --ct0 flags');
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import type { Command } from 'commander';
|
||||
import type { CliContext } from '../cli/shared.js';
|
||||
|
||||
export function registerHelpCommand(program: Command, ctx: CliContext): void {
|
||||
program
|
||||
.command('help [command]')
|
||||
.description('Show help for a command')
|
||||
.action((commandName?: string) => {
|
||||
if (!commandName) {
|
||||
program.outputHelp();
|
||||
return;
|
||||
}
|
||||
|
||||
const cmd = program.commands.find((c) => c.name() === commandName);
|
||||
if (!cmd) {
|
||||
console.error(`${ctx.p('err')}Unknown command: ${commandName}`);
|
||||
process.exitCode = 2;
|
||||
return;
|
||||
}
|
||||
|
||||
cmd.outputHelp();
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
import type { Command } from 'commander';
|
||||
import type { CliContext, MediaSpec } from '../cli/shared.js';
|
||||
import { formatTweetUrlLine } from '../lib/output.js';
|
||||
import { TwitterClient } from '../lib/twitter-client.js';
|
||||
|
||||
async function uploadMediaOrExit(
|
||||
client: TwitterClient,
|
||||
media: MediaSpec[],
|
||||
ctx: CliContext,
|
||||
): Promise<string[] | undefined> {
|
||||
if (media.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const uploaded: string[] = [];
|
||||
for (const item of media) {
|
||||
const res = await client.uploadMedia({ data: item.buffer, mimeType: item.mime, alt: item.alt });
|
||||
if (!res.success || !res.mediaId) {
|
||||
console.error(`${ctx.p('err')}Media upload failed: ${res.error ?? 'Unknown error'}`);
|
||||
process.exit(1);
|
||||
}
|
||||
uploaded.push(res.mediaId);
|
||||
}
|
||||
return uploaded;
|
||||
}
|
||||
|
||||
export function registerPostCommands(program: Command, ctx: CliContext): void {
|
||||
program
|
||||
.command('tweet')
|
||||
.description('Post a new tweet')
|
||||
.argument('<text>', 'Tweet text')
|
||||
.action(async (text: string) => {
|
||||
const opts = program.opts();
|
||||
const timeoutMs = ctx.resolveTimeoutFromOptions(opts);
|
||||
const quoteDepth = ctx.resolveQuoteDepthFromOptions(opts);
|
||||
let media: MediaSpec[] = [];
|
||||
try {
|
||||
media = ctx.loadMedia({ media: opts.media ?? [], alts: opts.alt ?? [] });
|
||||
} catch (error) {
|
||||
console.error(`${ctx.p('err')}${error instanceof Error ? error.message : String(error)}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const { cookies, warnings } = await ctx.resolveCredentialsFromOptions(opts);
|
||||
|
||||
for (const warning of warnings) {
|
||||
console.error(`${ctx.p('warn')}${warning}`);
|
||||
}
|
||||
|
||||
if (!cookies.authToken || !cookies.ct0) {
|
||||
console.error(`${ctx.p('err')}Missing required credentials`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (cookies.source) {
|
||||
console.error(`${ctx.l('source')}${cookies.source}`);
|
||||
}
|
||||
|
||||
const client = new TwitterClient({ cookies, timeoutMs, quoteDepth });
|
||||
const mediaIds = await uploadMediaOrExit(client, media, ctx);
|
||||
const result = await client.tweet(text, mediaIds);
|
||||
|
||||
if (result.success) {
|
||||
console.log(`${ctx.p('ok')}Tweet posted successfully!`);
|
||||
console.log(formatTweetUrlLine(result.tweetId, ctx.getOutput()));
|
||||
} else {
|
||||
console.error(`${ctx.p('err')}Failed to post tweet: ${result.error}`);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
program
|
||||
.command('reply')
|
||||
.description('Reply to an existing tweet')
|
||||
.argument('<tweet-id-or-url>', 'Tweet ID or URL to reply to')
|
||||
.argument('<text>', 'Reply text')
|
||||
.action(async (tweetIdOrUrl: string, text: string) => {
|
||||
const opts = program.opts();
|
||||
const timeoutMs = ctx.resolveTimeoutFromOptions(opts);
|
||||
const quoteDepth = ctx.resolveQuoteDepthFromOptions(opts);
|
||||
let media: MediaSpec[] = [];
|
||||
try {
|
||||
media = ctx.loadMedia({ media: opts.media ?? [], alts: opts.alt ?? [] });
|
||||
} catch (error) {
|
||||
console.error(`${ctx.p('err')}${error instanceof Error ? error.message : String(error)}`);
|
||||
process.exit(1);
|
||||
}
|
||||
const tweetId = ctx.extractTweetId(tweetIdOrUrl);
|
||||
|
||||
const { cookies, warnings } = await ctx.resolveCredentialsFromOptions(opts);
|
||||
|
||||
for (const warning of warnings) {
|
||||
console.error(`${ctx.p('warn')}${warning}`);
|
||||
}
|
||||
|
||||
if (!cookies.authToken || !cookies.ct0) {
|
||||
console.error(`${ctx.p('err')}Missing required credentials`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (cookies.source) {
|
||||
console.error(`${ctx.l('source')}${cookies.source}`);
|
||||
}
|
||||
|
||||
console.error(`${ctx.p('info')}Replying to tweet: ${tweetId}`);
|
||||
|
||||
const client = new TwitterClient({ cookies, timeoutMs, quoteDepth });
|
||||
const mediaIds = await uploadMediaOrExit(client, media, ctx);
|
||||
const result = await client.reply(text, tweetId, mediaIds);
|
||||
|
||||
if (result.success) {
|
||||
console.log(`${ctx.p('ok')}Reply posted successfully!`);
|
||||
console.log(formatTweetUrlLine(result.tweetId, ctx.getOutput()));
|
||||
} else {
|
||||
console.error(`${ctx.p('err')}Failed to post reply: ${result.error}`);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import type { Command } from 'commander';
|
||||
import type { CliContext } from '../cli/shared.js';
|
||||
import { runtimeQueryIds } from '../lib/runtime-query-ids.js';
|
||||
|
||||
export function registerQueryIdsCommand(program: Command, ctx: CliContext): void {
|
||||
program
|
||||
.command('query-ids')
|
||||
.description('Show or refresh cached Twitter GraphQL query IDs')
|
||||
.option('--json', 'Output as JSON')
|
||||
.option('--fresh', 'Force refresh (downloads X client bundles)', false)
|
||||
.action(async (cmdOpts: { json?: boolean; fresh?: boolean }) => {
|
||||
const operations = [
|
||||
'CreateTweet',
|
||||
'CreateRetweet',
|
||||
'FavoriteTweet',
|
||||
'TweetDetail',
|
||||
'SearchTimeline',
|
||||
'UserArticlesTweets',
|
||||
'Bookmarks',
|
||||
'Following',
|
||||
'Followers',
|
||||
'Likes',
|
||||
];
|
||||
|
||||
if (cmdOpts.fresh) {
|
||||
console.error(`${ctx.p('info')}Refreshing GraphQL query IDs…`);
|
||||
await runtimeQueryIds.refresh(operations, { force: true });
|
||||
}
|
||||
|
||||
const info = await runtimeQueryIds.getSnapshotInfo();
|
||||
if (!info) {
|
||||
if (cmdOpts.json) {
|
||||
console.log(JSON.stringify({ cached: false, cachePath: runtimeQueryIds.cachePath }, null, 2));
|
||||
return;
|
||||
}
|
||||
console.log(`${ctx.p('warn')}No cached query IDs yet.`);
|
||||
console.log(`${ctx.p('info')}Run: bird query-ids --fresh`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmdOpts.json) {
|
||||
console.log(
|
||||
JSON.stringify(
|
||||
{
|
||||
cached: true,
|
||||
cachePath: info.cachePath,
|
||||
fetchedAt: info.snapshot.fetchedAt,
|
||||
isFresh: info.isFresh,
|
||||
ageMs: info.ageMs,
|
||||
ids: info.snapshot.ids,
|
||||
discovery: info.snapshot.discovery,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`${ctx.p('ok')}GraphQL query IDs cached`);
|
||||
console.log(`path: ${info.cachePath}`);
|
||||
console.log(`fetched_at: ${info.snapshot.fetchedAt}`);
|
||||
console.log(`fresh: ${info.isFresh ? 'yes' : 'no'}`);
|
||||
console.log(`ops: ${Object.keys(info.snapshot.ids).length}`);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
import type { Command } from 'commander';
|
||||
import type { CliContext } from '../cli/shared.js';
|
||||
import { formatStatsLine } from '../lib/output.js';
|
||||
import { TwitterClient } from '../lib/twitter-client.js';
|
||||
|
||||
export function registerReadCommands(program: Command, ctx: CliContext): void {
|
||||
program
|
||||
.command('read')
|
||||
.description('Read/fetch a tweet by ID or URL')
|
||||
.argument('<tweet-id-or-url>', 'Tweet ID or URL to read')
|
||||
.option('--json', 'Output as JSON')
|
||||
.action(async (tweetIdOrUrl: string, cmdOpts: { json?: boolean }) => {
|
||||
const opts = program.opts();
|
||||
const timeoutMs = ctx.resolveTimeoutFromOptions(opts);
|
||||
const quoteDepth = ctx.resolveQuoteDepthFromOptions(opts);
|
||||
|
||||
const tweetId = ctx.extractTweetId(tweetIdOrUrl);
|
||||
|
||||
const { cookies, warnings } = await ctx.resolveCredentialsFromOptions(opts);
|
||||
|
||||
for (const warning of warnings) {
|
||||
console.error(`${ctx.p('warn')}${warning}`);
|
||||
}
|
||||
|
||||
if (!cookies.authToken || !cookies.ct0) {
|
||||
console.error(`${ctx.p('err')}Missing required credentials`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const client = new TwitterClient({ cookies, timeoutMs, quoteDepth });
|
||||
const result = await client.getTweet(tweetId);
|
||||
|
||||
if (result.success && result.tweet) {
|
||||
if (cmdOpts.json) {
|
||||
console.log(JSON.stringify(result.tweet, null, 2));
|
||||
} else {
|
||||
console.log(`@${result.tweet.author.username} (${result.tweet.author.name}):`);
|
||||
console.log(result.tweet.text);
|
||||
if (result.tweet.createdAt) {
|
||||
console.log(`\n${ctx.l('date')}${result.tweet.createdAt}`);
|
||||
}
|
||||
console.log(formatStatsLine(result.tweet, ctx.getOutput()));
|
||||
}
|
||||
} else {
|
||||
console.error(`${ctx.p('err')}Failed to read tweet: ${result.error}`);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
program
|
||||
.command('replies')
|
||||
.description('List replies to a tweet (by ID or URL)')
|
||||
.argument('<tweet-id-or-url>', 'Tweet ID or URL')
|
||||
.option('--json', 'Output as JSON')
|
||||
.action(async (tweetIdOrUrl: string, cmdOpts: { json?: boolean }) => {
|
||||
const opts = program.opts();
|
||||
const timeoutMs = ctx.resolveTimeoutFromOptions(opts);
|
||||
const quoteDepth = ctx.resolveQuoteDepthFromOptions(opts);
|
||||
const tweetId = ctx.extractTweetId(tweetIdOrUrl);
|
||||
|
||||
const { cookies, warnings } = await ctx.resolveCredentialsFromOptions(opts);
|
||||
|
||||
for (const warning of warnings) {
|
||||
console.error(`${ctx.p('warn')}${warning}`);
|
||||
}
|
||||
|
||||
if (!cookies.authToken || !cookies.ct0) {
|
||||
console.error(`${ctx.p('err')}Missing required credentials`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const client = new TwitterClient({ cookies, timeoutMs, quoteDepth });
|
||||
const result = await client.getReplies(tweetId);
|
||||
|
||||
if (result.success && result.tweets) {
|
||||
ctx.printTweets(result.tweets, { json: cmdOpts.json, emptyMessage: 'No replies found.' });
|
||||
} else {
|
||||
console.error(`${ctx.p('err')}Failed to fetch replies: ${result.error}`);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
program
|
||||
.command('thread')
|
||||
.description('Show the full conversation thread containing the tweet')
|
||||
.argument('<tweet-id-or-url>', 'Tweet ID or URL')
|
||||
.option('--json', 'Output as JSON')
|
||||
.action(async (tweetIdOrUrl: string, cmdOpts: { json?: boolean }) => {
|
||||
const opts = program.opts();
|
||||
const timeoutMs = ctx.resolveTimeoutFromOptions(opts);
|
||||
const quoteDepth = ctx.resolveQuoteDepthFromOptions(opts);
|
||||
const tweetId = ctx.extractTweetId(tweetIdOrUrl);
|
||||
|
||||
const { cookies, warnings } = await ctx.resolveCredentialsFromOptions(opts);
|
||||
|
||||
for (const warning of warnings) {
|
||||
console.error(`${ctx.p('warn')}${warning}`);
|
||||
}
|
||||
|
||||
if (!cookies.authToken || !cookies.ct0) {
|
||||
console.error(`${ctx.p('err')}Missing required credentials`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const client = new TwitterClient({ cookies, timeoutMs, quoteDepth });
|
||||
const result = await client.getThread(tweetId);
|
||||
|
||||
if (result.success && result.tweets) {
|
||||
ctx.printTweets(result.tweets, { json: cmdOpts.json, emptyMessage: 'No thread tweets found.' });
|
||||
} else {
|
||||
console.error(`${ctx.p('err')}Failed to fetch thread: ${result.error}`);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
import type { Command } from 'commander';
|
||||
import type { CliContext } from '../cli/shared.js';
|
||||
import { mentionsQueryFromUserOption, normalizeHandle } from '../lib/normalize-handle.js';
|
||||
import { TwitterClient } from '../lib/twitter-client.js';
|
||||
|
||||
export function registerSearchCommands(program: Command, ctx: CliContext): void {
|
||||
program
|
||||
.command('search')
|
||||
.description('Search for tweets')
|
||||
.argument('<query>', 'Search query (e.g., "@clawdbot" or "from:clawdbot")')
|
||||
.option('-n, --count <number>', 'Number of tweets to fetch', '10')
|
||||
.option('--json', 'Output as JSON')
|
||||
.action(async (query: string, cmdOpts: { count?: string; json?: boolean }) => {
|
||||
const opts = program.opts();
|
||||
const timeoutMs = ctx.resolveTimeoutFromOptions(opts);
|
||||
const quoteDepth = ctx.resolveQuoteDepthFromOptions(opts);
|
||||
const count = Number.parseInt(cmdOpts.count || '10', 10);
|
||||
|
||||
const { cookies, warnings } = await ctx.resolveCredentialsFromOptions(opts);
|
||||
|
||||
for (const warning of warnings) {
|
||||
console.error(`${ctx.p('warn')}${warning}`);
|
||||
}
|
||||
|
||||
if (!cookies.authToken || !cookies.ct0) {
|
||||
console.error(`${ctx.p('err')}Missing required credentials`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const client = new TwitterClient({ cookies, timeoutMs, quoteDepth });
|
||||
const result = await client.search(query, count);
|
||||
|
||||
if (result.success && result.tweets) {
|
||||
ctx.printTweets(result.tweets, { json: cmdOpts.json, emptyMessage: 'No tweets found.' });
|
||||
} else {
|
||||
console.error(`${ctx.p('err')}Search failed: ${result.error}`);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
program
|
||||
.command('mentions')
|
||||
.description('Find tweets mentioning a user (defaults to current user)')
|
||||
.option('-u, --user <handle>', 'User handle (e.g. @steipete)')
|
||||
.option('-n, --count <number>', 'Number of tweets to fetch', '10')
|
||||
.option('--json', 'Output as JSON')
|
||||
.action(async (cmdOpts: { user?: string; count?: string; json?: boolean }) => {
|
||||
const opts = program.opts();
|
||||
const timeoutMs = ctx.resolveTimeoutFromOptions(opts);
|
||||
const quoteDepth = ctx.resolveQuoteDepthFromOptions(opts);
|
||||
const count = Number.parseInt(cmdOpts.count || '10', 10);
|
||||
|
||||
const fromUserOpt = mentionsQueryFromUserOption(cmdOpts.user);
|
||||
if (fromUserOpt.error) {
|
||||
console.error(`${ctx.p('err')}${fromUserOpt.error}`);
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
let query: string | null = fromUserOpt.query;
|
||||
|
||||
const { cookies, warnings } = await ctx.resolveCredentialsFromOptions(opts);
|
||||
|
||||
for (const warning of warnings) {
|
||||
console.error(`${ctx.p('warn')}${warning}`);
|
||||
}
|
||||
|
||||
if (!cookies.authToken || !cookies.ct0) {
|
||||
console.error(`${ctx.p('err')}Missing required credentials`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const client = new TwitterClient({ cookies, timeoutMs, quoteDepth });
|
||||
|
||||
if (!query) {
|
||||
const who = await client.getCurrentUser();
|
||||
const handle = normalizeHandle(who.user?.username);
|
||||
if (handle) {
|
||||
query = `@${handle}`;
|
||||
} else {
|
||||
console.error(
|
||||
`${ctx.p('err')}Could not determine current user (${who.error ?? 'Unknown error'}). Use --user <handle>.`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
const result = await client.search(query, count);
|
||||
|
||||
if (result.success && result.tweets) {
|
||||
ctx.printTweets(result.tweets, { json: cmdOpts.json, emptyMessage: 'No mentions found.' });
|
||||
} else {
|
||||
console.error(`${ctx.p('err')}Failed to fetch mentions: ${result.error}`);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
import type { Command } from 'commander';
|
||||
import type { CliContext } from '../cli/shared.js';
|
||||
import { TwitterClient } from '../lib/twitter-client.js';
|
||||
|
||||
export function registerUserCommands(program: Command, ctx: CliContext): void {
|
||||
program
|
||||
.command('following')
|
||||
.description('Get users that you (or another user) follow')
|
||||
.option('--user <userId>', 'User ID to get following for (defaults to current user)')
|
||||
.option('-n, --count <number>', 'Number of users to fetch', '20')
|
||||
.option('--json', 'Output as JSON')
|
||||
.action(async (cmdOpts: { user?: string; count?: string; json?: boolean }) => {
|
||||
const opts = program.opts();
|
||||
const timeoutMs = ctx.resolveTimeoutFromOptions(opts);
|
||||
const count = Number.parseInt(cmdOpts.count || '20', 10);
|
||||
|
||||
const { cookies, warnings } = await ctx.resolveCredentialsFromOptions(opts);
|
||||
|
||||
for (const warning of warnings) {
|
||||
console.error(`${ctx.p('warn')}${warning}`);
|
||||
}
|
||||
|
||||
if (!cookies.authToken || !cookies.ct0) {
|
||||
console.error(`${ctx.p('err')}Missing required credentials`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const client = new TwitterClient({ cookies, timeoutMs });
|
||||
|
||||
let userId = cmdOpts.user;
|
||||
if (!userId) {
|
||||
const currentUser = await client.getCurrentUser();
|
||||
if (!currentUser.success || !currentUser.user?.id) {
|
||||
console.error(`${ctx.p('err')}Failed to get current user: ${currentUser.error || 'Unknown error'}`);
|
||||
process.exit(1);
|
||||
}
|
||||
userId = currentUser.user.id;
|
||||
}
|
||||
|
||||
const result = await client.getFollowing(userId, count);
|
||||
|
||||
if (result.success && result.users) {
|
||||
if (cmdOpts.json) {
|
||||
console.log(JSON.stringify(result.users, null, 2));
|
||||
} else {
|
||||
if (result.users.length === 0) {
|
||||
console.log('No users found.');
|
||||
} else {
|
||||
for (const user of result.users) {
|
||||
console.log(`@${user.username} (${user.name})`);
|
||||
if (user.description) {
|
||||
console.log(` ${user.description.slice(0, 100)}${user.description.length > 100 ? '...' : ''}`);
|
||||
}
|
||||
if (user.followersCount !== undefined) {
|
||||
console.log(` ${ctx.p('info')}${user.followersCount.toLocaleString()} followers`);
|
||||
}
|
||||
console.log('──────────────────────────────────────────────────');
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.error(`${ctx.p('err')}Failed to fetch following: ${result.error}`);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
program
|
||||
.command('followers')
|
||||
.description('Get users that follow you (or another user)')
|
||||
.option('--user <userId>', 'User ID to get followers for (defaults to current user)')
|
||||
.option('-n, --count <number>', 'Number of users to fetch', '20')
|
||||
.option('--json', 'Output as JSON')
|
||||
.action(async (cmdOpts: { user?: string; count?: string; json?: boolean }) => {
|
||||
const opts = program.opts();
|
||||
const timeoutMs = ctx.resolveTimeoutFromOptions(opts);
|
||||
const count = Number.parseInt(cmdOpts.count || '20', 10);
|
||||
|
||||
const { cookies, warnings } = await ctx.resolveCredentialsFromOptions(opts);
|
||||
|
||||
for (const warning of warnings) {
|
||||
console.error(`${ctx.p('warn')}${warning}`);
|
||||
}
|
||||
|
||||
if (!cookies.authToken || !cookies.ct0) {
|
||||
console.error(`${ctx.p('err')}Missing required credentials`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const client = new TwitterClient({ cookies, timeoutMs });
|
||||
|
||||
let userId = cmdOpts.user;
|
||||
if (!userId) {
|
||||
const currentUser = await client.getCurrentUser();
|
||||
if (!currentUser.success || !currentUser.user?.id) {
|
||||
console.error(`${ctx.p('err')}Failed to get current user: ${currentUser.error || 'Unknown error'}`);
|
||||
process.exit(1);
|
||||
}
|
||||
userId = currentUser.user.id;
|
||||
}
|
||||
|
||||
const result = await client.getFollowers(userId, count);
|
||||
|
||||
if (result.success && result.users) {
|
||||
if (cmdOpts.json) {
|
||||
console.log(JSON.stringify(result.users, null, 2));
|
||||
} else {
|
||||
if (result.users.length === 0) {
|
||||
console.log('No users found.');
|
||||
} else {
|
||||
for (const user of result.users) {
|
||||
console.log(`@${user.username} (${user.name})`);
|
||||
if (user.description) {
|
||||
console.log(` ${user.description.slice(0, 100)}${user.description.length > 100 ? '...' : ''}`);
|
||||
}
|
||||
if (user.followersCount !== undefined) {
|
||||
console.log(` ${ctx.p('info')}${user.followersCount.toLocaleString()} followers`);
|
||||
}
|
||||
console.log('──────────────────────────────────────────────────');
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.error(`${ctx.p('err')}Failed to fetch followers: ${result.error}`);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
program
|
||||
.command('likes')
|
||||
.description('Get your liked tweets')
|
||||
.option('-n, --count <number>', 'Number of likes to fetch', '20')
|
||||
.option('--json', 'Output as JSON')
|
||||
.action(async (cmdOpts: { count?: string; json?: boolean }) => {
|
||||
const opts = program.opts();
|
||||
const timeoutMs = ctx.resolveTimeoutFromOptions(opts);
|
||||
const quoteDepth = ctx.resolveQuoteDepthFromOptions(opts);
|
||||
const count = Number.parseInt(cmdOpts.count || '20', 10);
|
||||
|
||||
const { cookies, warnings } = await ctx.resolveCredentialsFromOptions(opts);
|
||||
|
||||
for (const warning of warnings) {
|
||||
console.error(`${ctx.p('warn')}${warning}`);
|
||||
}
|
||||
|
||||
if (!cookies.authToken || !cookies.ct0) {
|
||||
console.error(`${ctx.p('err')}Missing required credentials`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const client = new TwitterClient({ cookies, timeoutMs, quoteDepth });
|
||||
const result = await client.getLikes(count);
|
||||
|
||||
if (result.success && result.tweets) {
|
||||
ctx.printTweets(result.tweets, { json: cmdOpts.json, emptyMessage: 'No liked tweets found.' });
|
||||
} else {
|
||||
console.error(`${ctx.p('err')}Failed to fetch likes: ${result.error}`);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
program
|
||||
.command('whoami')
|
||||
.description('Show which Twitter account the current credentials belong to')
|
||||
.action(async () => {
|
||||
const opts = program.opts();
|
||||
const timeoutMs = ctx.resolveTimeoutFromOptions(opts);
|
||||
const quoteDepth = ctx.resolveQuoteDepthFromOptions(opts);
|
||||
|
||||
const { cookies, warnings } = await ctx.resolveCredentialsFromOptions(opts);
|
||||
|
||||
for (const warning of warnings) {
|
||||
console.error(`${ctx.p('warn')}${warning}`);
|
||||
}
|
||||
|
||||
if (!cookies.authToken || !cookies.ct0) {
|
||||
console.error(`${ctx.p('err')}Missing required credentials`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (cookies.source) {
|
||||
console.error(`${ctx.l('source')}${cookies.source}`);
|
||||
}
|
||||
|
||||
const client = new TwitterClient({ cookies, timeoutMs, quoteDepth });
|
||||
const result = await client.getCurrentUser();
|
||||
|
||||
const credentialSource = cookies.source ?? 'env/auto-detected cookies';
|
||||
|
||||
if (result.success && result.user) {
|
||||
console.log(`${ctx.l('user')}@${result.user.username} (${result.user.name})`);
|
||||
console.log(`${ctx.l('userId')}${result.user.id}`);
|
||||
console.log(`${ctx.l('engine')}graphql`);
|
||||
console.log(`${ctx.l('credentials')}${credentialSource}`);
|
||||
} else {
|
||||
console.error(`${ctx.p('err')}Failed to determine current user: ${result.error ?? 'Unknown error'}`);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user