From a94a5edc11e7393330214bbefc32cb4b61c03c40 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Wed, 3 Dec 2025 16:09:44 +0000 Subject: [PATCH] chore: align biome lint config and clean warnings --- biome.json | 2 +- package.json | 1 + src/index.ts | 34 +++++++++++++++++----------------- src/lib/cookies.ts | 4 +++- src/lib/sweetistics-client.ts | 34 +++++++++++++++++++++++++++------- src/lib/twitter-client.ts | 7 +++---- tests/twitter-client.test.ts | 2 +- 7 files changed, 53 insertions(+), 31 deletions(-) diff --git a/biome.json b/biome.json index 4ab1168..0ab0c5f 100644 --- a/biome.json +++ b/biome.json @@ -28,6 +28,6 @@ } }, "files": { - "ignore": ["dist/", "node_modules/", "*.d.ts"] + "ignore": ["dist/", "coverage/", "node_modules/", "scripts/update-query-ids.ts", "*.d.ts"] } } diff --git a/package.json b/package.json index e0ea85a..db08be4 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "devDependencies": { "@biomejs/biome": "^1.9.4", "@types/node": "^22.10.0", + "@vitest/coverage-v8": "2.1.9", "tsx": "^4.19.2", "typescript": "^5.7.2", "vitest": "^2.1.6" diff --git a/src/index.ts b/src/index.ts index d1c0ae3..3c16d28 100644 --- a/src/index.ts +++ b/src/index.ts @@ -9,16 +9,16 @@ * bird read */ +import { existsSync, readFileSync } from 'node:fs'; +import { homedir } from 'node:os'; +import { join } from 'node:path'; import { Command } from 'commander'; import JSON5 from 'json5'; import kleur from 'kleur'; -import { homedir } from 'node:os'; -import { existsSync, readFileSync } from 'node:fs'; -import { join } from 'node:path'; import { resolveCredentials } from './lib/cookies.js'; import { extractTweetId } from './lib/extract-tweet-id.js'; -import { TwitterClient, type TweetData } from './lib/twitter-client.js'; import { SweetisticsClient } from './lib/sweetistics-client.js'; +import { type TweetData, TwitterClient } from './lib/twitter-client.js'; const program = new Command(); @@ -53,7 +53,9 @@ function readConfigFile(path: string): Partial { const parsed = JSON5.parse(raw) as Partial; return parsed ?? {}; } catch (error) { - console.error(colors.muted(`⚠️ Failed to parse config at ${path}: ${error instanceof Error ? error.message : String(error)}`)); + console.error( + colors.muted(`⚠️ Failed to parse config at ${path}: ${error instanceof Error ? error.message : String(error)}`), + ); return {}; } } @@ -82,12 +84,13 @@ const formatExample = (command: string, description: string) => program.addHelpText( 'afterAll', - () => `\n${colors.section('Examples')}\n${[ - formatExample('bird whoami', 'Show the logged-in account via GraphQL cookies'), - formatExample('bird --firefox-profile default-release whoami', 'Use Firefox profile cookies'), - formatExample('bird tweet "hello from bird"', 'Send a tweet'), - formatExample('bird replies https://x.com/user/status/1234567890123456789', 'Check replies to a tweet'), - ].join('\n\n')}`, + () => + `\n${colors.section('Examples')}\n${[ + formatExample('bird whoami', 'Show the logged-in account via GraphQL cookies'), + formatExample('bird --firefox-profile default-release whoami', 'Use Firefox profile cookies'), + formatExample('bird tweet "hello from bird"', 'Send a tweet'), + formatExample('bird replies https://x.com/user/status/1234567890123456789', 'Check replies to a tweet'), + ].join('\n\n')}`, ); // Global options for authentication @@ -112,10 +115,7 @@ type EngineMode = 'graphql' | 'sweetistics' | 'auto'; function resolveSweetisticsConfig(options: { sweetisticsApiKey?: string; sweetisticsBaseUrl?: string }) { const apiKey = - options.sweetisticsApiKey || - process.env.SWEETISTICS_API_KEY || - process.env.SWEETISTICS_LOCALHOST_API_KEY || - null; + options.sweetisticsApiKey || process.env.SWEETISTICS_API_KEY || process.env.SWEETISTICS_LOCALHOST_API_KEY || null; const baseUrl = options.sweetisticsBaseUrl || process.env.SWEETISTICS_BASE_URL || 'https://sweetistics.com'; @@ -512,7 +512,7 @@ program .option('--json', 'Output as JSON') .action(async (query: string, cmdOpts: { count?: string; json?: boolean }) => { const opts = program.opts(); - const count = parseInt(cmdOpts.count || '10', 10); + const count = Number.parseInt(cmdOpts.count || '10', 10); const sweetistics = resolveSweetisticsConfig({ sweetisticsApiKey: opts.sweetisticsApiKey || config.sweetisticsApiKey, sweetisticsBaseUrl: opts.sweetisticsBaseUrl || config.sweetisticsBaseUrl, @@ -570,7 +570,7 @@ program .option('--json', 'Output as JSON') .action(async (cmdOpts: { count?: string; json?: boolean }) => { const opts = program.opts(); - const count = parseInt(cmdOpts.count || '10', 10); + const count = Number.parseInt(cmdOpts.count || '10', 10); const sweetistics = resolveSweetisticsConfig(opts); const engine = resolveEngineMode(opts.engine); const useSweetistics = shouldUseSweetistics(engine, Boolean(sweetistics.apiKey)); diff --git a/src/lib/cookies.ts b/src/lib/cookies.ts index 1825902..6a88107 100644 --- a/src/lib/cookies.ts +++ b/src/lib/cookies.ts @@ -351,7 +351,9 @@ export async function resolveCredentials(options: { // Validation if (!cookies.authToken) { - warnings.push('Missing auth_token - provide via --auth-token, AUTH_TOKEN env var, or login to x.com in Chrome/Firefox'); + warnings.push( + 'Missing auth_token - provide via --auth-token, AUTH_TOKEN env var, or login to x.com in Chrome/Firefox', + ); } if (!cookies.ct0) { warnings.push('Missing ct0 - provide via --ct0, CT0 env var, or login to x.com in Chrome/Firefox'); diff --git a/src/lib/sweetistics-client.ts b/src/lib/sweetistics-client.ts index 609a937..41a6b0e 100644 --- a/src/lib/sweetistics-client.ts +++ b/src/lib/sweetistics-client.ts @@ -99,9 +99,12 @@ export class SweetisticsClient { }; } - const success = typeof (data as { success?: unknown })?.success === 'boolean' ? (data as { success: boolean }).success : false; - const tweetId = typeof (data as { tweetId?: unknown })?.tweetId === 'string' ? (data as { tweetId?: string }).tweetId : undefined; - const errorMessage = typeof (data as { error?: unknown })?.error === 'string' ? (data as { error?: string }).error : undefined; + const success = + typeof (data as { success?: unknown })?.success === 'boolean' ? (data as { success: boolean }).success : false; + const tweetId = + typeof (data as { tweetId?: unknown })?.tweetId === 'string' ? (data as { tweetId?: string }).tweetId : undefined; + const errorMessage = + typeof (data as { error?: unknown })?.error === 'string' ? (data as { error?: string }).error : undefined; if (!response.ok || !success) { const reason = errorMessage || `HTTP ${response.status}`; @@ -125,6 +128,7 @@ export class SweetisticsClient { return { success: false, error: `HTTP ${response.status}` }; } + // biome-ignore lint/suspicious/noExplicitAny: Sweetistics responses are loosely typed let data: any; try { data = await response.json(); @@ -151,6 +155,7 @@ export class SweetisticsClient { retweetCount: data.metrics?.retweetCount, likeCount: data.metrics?.likeCount, conversationId: data.conversationId, + // biome-ignore lint/suspicious/noExplicitAny: Sweetistics responses are loosely typed inReplyToStatusId: data.referencedTweets?.find?.((r: any) => r?.type === 'replied_to')?.id, }; @@ -194,6 +199,7 @@ export class SweetisticsClient { return { success: false, error: `HTTP ${response.status}` }; } + // biome-ignore lint/suspicious/noExplicitAny: Sweetistics responses are loosely typed let body: any; try { body = await response.json(); @@ -214,6 +220,7 @@ export class SweetisticsClient { return { success: false, error: 'Missing data in Sweetistics search response' }; } + // biome-ignore lint/suspicious/noExplicitAny: Sweetistics responses are loosely typed let parsed: any; try { parsed = typeof dataField === 'string' ? devalueParse(dataField) : dataField; @@ -225,6 +232,7 @@ export class SweetisticsClient { } const tweetsSection = parsed?.tweets; + // biome-ignore lint/suspicious/noExplicitAny: Sweetistics responses are loosely typed const items: any[] = tweetsSection?.items ?? []; if (items.length === 0) { return { success: true, tweets: [] }; @@ -236,8 +244,8 @@ export class SweetisticsClient { const createdAt = typeof item.createdAt === 'string' ? item.createdAt - : item.createdAt?.toString?.() ?? - (Array.isArray(item.createdAt) && item.createdAt[0] === 'Date' ? item.createdAt[1] : undefined); + : (item.createdAt?.toString?.() ?? + (Array.isArray(item.createdAt) && item.createdAt[0] === 'Date' ? item.createdAt[1] : undefined)); const metrics = item.full?.metrics ?? item.full ?? item; return { @@ -281,6 +289,7 @@ export class SweetisticsClient { return { success: false, error: `HTTP ${response.status}` }; } + // biome-ignore lint/suspicious/noExplicitAny: Sweetistics responses are loosely typed let body: any; try { body = await response.json(); @@ -314,6 +323,7 @@ export class SweetisticsClient { } const user = data && typeof data === 'object' ? data : null; + // biome-ignore lint/suspicious/noExplicitAny: Sweetistics responses are loosely typed if (!user || typeof (user as any).id !== 'string') { return { success: false, error: 'Malformed user payload from Sweetistics' }; } @@ -321,10 +331,16 @@ export class SweetisticsClient { return { success: true, user: { + // biome-ignore lint/suspicious/noExplicitAny: Sweetistics responses are loosely typed id: String((user as any).id), - username: (user as any).username ?? (user as any).twitterUsername ?? null, + username: + // biome-ignore lint/suspicious/noExplicitAny: Sweetistics responses are loosely typed + (user as any).username ?? (user as any).twitterUsername ?? null, + // biome-ignore lint/suspicious/noExplicitAny: Sweetistics responses are loosely typed name: (user as any).name ?? null, + // biome-ignore lint/suspicious/noExplicitAny: Sweetistics responses are loosely typed email: (user as any).email ?? null, + // biome-ignore lint/suspicious/noExplicitAny: Sweetistics responses are loosely typed profileImageUrl: (user as any).profileImageUrl ?? null, }, }; @@ -362,9 +378,11 @@ export class SweetisticsClient { return { success: false, error: 'Missing data in Sweetistics response' }; } + // biome-ignore lint/suspicious/noExplicitAny: Sweetistics responses are loosely typed let parsed: any; try { - const raw = typeof envelope.result.data === 'string' ? devalueParse(envelope.result.data as string) : envelope.result.data; + const raw = + typeof envelope.result.data === 'string' ? devalueParse(envelope.result.data as string) : envelope.result.data; parsed = raw; } catch (error) { return { @@ -374,6 +392,7 @@ export class SweetisticsClient { } const tweetIds: string[] = Array.isArray(parsed?.tweetIds) ? parsed.tweetIds.map(String) : []; + // biome-ignore lint/suspicious/noExplicitAny: Sweetistics responses are loosely typed const tweetsMap: Record = parsed?.tweets ?? {}; if (tweetIds.length === 0 || Object.keys(tweetsMap).length === 0) { @@ -386,6 +405,7 @@ export class SweetisticsClient { const tweets: SweetisticsTweet[] = filteredIds .map((id) => tweetsMap[id]) .filter(Boolean) + // biome-ignore lint/suspicious/noExplicitAny: Sweetistics responses are loosely typed .map((item: any) => ({ id: String(item.id), text: String(item.text ?? ''), diff --git a/src/lib/twitter-client.ts b/src/lib/twitter-client.ts index c2b56cc..1a79de9 100644 --- a/src/lib/twitter-client.ts +++ b/src/lib/twitter-client.ts @@ -237,9 +237,7 @@ export class TwitterClient { return tweets; } - private async fetchTweetDetail( - tweetId: string, - ): Promise< + private async fetchTweetDetail(tweetId: string): Promise< | { success: true; data: { @@ -719,6 +717,7 @@ export class TwitterClient { continue; } + // biome-ignore lint/suspicious/noExplicitAny: Twitter API response is dynamic here let data: any; try { data = await response.json(); @@ -739,7 +738,7 @@ export class TwitterClient { ? data.name : typeof data?.user?.name === 'string' ? data.user.name - : username ?? ''; + : (username ?? ''); const userId = typeof data?.user_id === 'string' diff --git a/tests/twitter-client.test.ts b/tests/twitter-client.test.ts index dd44f29..13f878d 100644 --- a/tests/twitter-client.test.ts +++ b/tests/twitter-client.test.ts @@ -302,7 +302,7 @@ describe('TwitterClient', () => { beforeEach(() => { mockFetch = vi.fn(); - global.fetch = mockFetch as any; + global.fetch = mockFetch as unknown as typeof fetch; }); it('returns mapped user details when present', async () => {