chore: align biome lint config and clean warnings

This commit is contained in:
Peter Steinberger
2025-12-03 16:09:44 +00:00
parent 3b3e04204b
commit a94a5edc11
7 changed files with 53 additions and 31 deletions
+1 -1
View File
@@ -28,6 +28,6 @@
} }
}, },
"files": { "files": {
"ignore": ["dist/", "node_modules/", "*.d.ts"] "ignore": ["dist/", "coverage/", "node_modules/", "scripts/update-query-ids.ts", "*.d.ts"]
} }
} }
+1
View File
@@ -28,6 +28,7 @@
"devDependencies": { "devDependencies": {
"@biomejs/biome": "^1.9.4", "@biomejs/biome": "^1.9.4",
"@types/node": "^22.10.0", "@types/node": "^22.10.0",
"@vitest/coverage-v8": "2.1.9",
"tsx": "^4.19.2", "tsx": "^4.19.2",
"typescript": "^5.7.2", "typescript": "^5.7.2",
"vitest": "^2.1.6" "vitest": "^2.1.6"
+12 -12
View File
@@ -9,16 +9,16 @@
* bird read <tweet-id-or-url> * bird read <tweet-id-or-url>
*/ */
import { existsSync, readFileSync } from 'node:fs';
import { homedir } from 'node:os';
import { join } from 'node:path';
import { Command } from 'commander'; import { Command } from 'commander';
import JSON5 from 'json5'; import JSON5 from 'json5';
import kleur from 'kleur'; 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 { resolveCredentials } from './lib/cookies.js';
import { extractTweetId } from './lib/extract-tweet-id.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 { SweetisticsClient } from './lib/sweetistics-client.js';
import { type TweetData, TwitterClient } from './lib/twitter-client.js';
const program = new Command(); const program = new Command();
@@ -53,7 +53,9 @@ function readConfigFile(path: string): Partial<BirdConfig> {
const parsed = JSON5.parse(raw) as Partial<BirdConfig>; const parsed = JSON5.parse(raw) as Partial<BirdConfig>;
return parsed ?? {}; return parsed ?? {};
} catch (error) { } 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 {}; return {};
} }
} }
@@ -82,7 +84,8 @@ const formatExample = (command: string, description: string) =>
program.addHelpText( program.addHelpText(
'afterAll', 'afterAll',
() => `\n${colors.section('Examples')}\n${[ () =>
`\n${colors.section('Examples')}\n${[
formatExample('bird whoami', 'Show the logged-in account via GraphQL cookies'), formatExample('bird whoami', 'Show the logged-in account via GraphQL cookies'),
formatExample('bird --firefox-profile default-release whoami', 'Use Firefox profile cookies'), formatExample('bird --firefox-profile default-release whoami', 'Use Firefox profile cookies'),
formatExample('bird tweet "hello from bird"', 'Send a tweet'), formatExample('bird tweet "hello from bird"', 'Send a tweet'),
@@ -112,10 +115,7 @@ type EngineMode = 'graphql' | 'sweetistics' | 'auto';
function resolveSweetisticsConfig(options: { sweetisticsApiKey?: string; sweetisticsBaseUrl?: string }) { function resolveSweetisticsConfig(options: { sweetisticsApiKey?: string; sweetisticsBaseUrl?: string }) {
const apiKey = const apiKey =
options.sweetisticsApiKey || options.sweetisticsApiKey || process.env.SWEETISTICS_API_KEY || process.env.SWEETISTICS_LOCALHOST_API_KEY || null;
process.env.SWEETISTICS_API_KEY ||
process.env.SWEETISTICS_LOCALHOST_API_KEY ||
null;
const baseUrl = options.sweetisticsBaseUrl || process.env.SWEETISTICS_BASE_URL || 'https://sweetistics.com'; const baseUrl = options.sweetisticsBaseUrl || process.env.SWEETISTICS_BASE_URL || 'https://sweetistics.com';
@@ -512,7 +512,7 @@ program
.option('--json', 'Output as JSON') .option('--json', 'Output as JSON')
.action(async (query: string, cmdOpts: { count?: string; json?: boolean }) => { .action(async (query: string, cmdOpts: { count?: string; json?: boolean }) => {
const opts = program.opts(); const opts = program.opts();
const count = parseInt(cmdOpts.count || '10', 10); const count = Number.parseInt(cmdOpts.count || '10', 10);
const sweetistics = resolveSweetisticsConfig({ const sweetistics = resolveSweetisticsConfig({
sweetisticsApiKey: opts.sweetisticsApiKey || config.sweetisticsApiKey, sweetisticsApiKey: opts.sweetisticsApiKey || config.sweetisticsApiKey,
sweetisticsBaseUrl: opts.sweetisticsBaseUrl || config.sweetisticsBaseUrl, sweetisticsBaseUrl: opts.sweetisticsBaseUrl || config.sweetisticsBaseUrl,
@@ -570,7 +570,7 @@ program
.option('--json', 'Output as JSON') .option('--json', 'Output as JSON')
.action(async (cmdOpts: { count?: string; json?: boolean }) => { .action(async (cmdOpts: { count?: string; json?: boolean }) => {
const opts = program.opts(); const opts = program.opts();
const count = parseInt(cmdOpts.count || '10', 10); const count = Number.parseInt(cmdOpts.count || '10', 10);
const sweetistics = resolveSweetisticsConfig(opts); const sweetistics = resolveSweetisticsConfig(opts);
const engine = resolveEngineMode(opts.engine); const engine = resolveEngineMode(opts.engine);
const useSweetistics = shouldUseSweetistics(engine, Boolean(sweetistics.apiKey)); const useSweetistics = shouldUseSweetistics(engine, Boolean(sweetistics.apiKey));
+3 -1
View File
@@ -351,7 +351,9 @@ export async function resolveCredentials(options: {
// Validation // Validation
if (!cookies.authToken) { 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) { if (!cookies.ct0) {
warnings.push('Missing ct0 - provide via --ct0, CT0 env var, or login to x.com in Chrome/Firefox'); warnings.push('Missing ct0 - provide via --ct0, CT0 env var, or login to x.com in Chrome/Firefox');
+27 -7
View File
@@ -99,9 +99,12 @@ export class SweetisticsClient {
}; };
} }
const success = typeof (data as { success?: unknown })?.success === 'boolean' ? (data as { success: boolean }).success : false; const success =
const tweetId = typeof (data as { tweetId?: unknown })?.tweetId === 'string' ? (data as { tweetId?: string }).tweetId : undefined; typeof (data as { success?: unknown })?.success === 'boolean' ? (data as { success: boolean }).success : false;
const errorMessage = typeof (data as { error?: unknown })?.error === 'string' ? (data as { error?: string }).error : undefined; 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) { if (!response.ok || !success) {
const reason = errorMessage || `HTTP ${response.status}`; const reason = errorMessage || `HTTP ${response.status}`;
@@ -125,6 +128,7 @@ export class SweetisticsClient {
return { success: false, error: `HTTP ${response.status}` }; return { success: false, error: `HTTP ${response.status}` };
} }
// biome-ignore lint/suspicious/noExplicitAny: Sweetistics responses are loosely typed
let data: any; let data: any;
try { try {
data = await response.json(); data = await response.json();
@@ -151,6 +155,7 @@ export class SweetisticsClient {
retweetCount: data.metrics?.retweetCount, retweetCount: data.metrics?.retweetCount,
likeCount: data.metrics?.likeCount, likeCount: data.metrics?.likeCount,
conversationId: data.conversationId, conversationId: data.conversationId,
// biome-ignore lint/suspicious/noExplicitAny: Sweetistics responses are loosely typed
inReplyToStatusId: data.referencedTweets?.find?.((r: any) => r?.type === 'replied_to')?.id, 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}` }; return { success: false, error: `HTTP ${response.status}` };
} }
// biome-ignore lint/suspicious/noExplicitAny: Sweetistics responses are loosely typed
let body: any; let body: any;
try { try {
body = await response.json(); body = await response.json();
@@ -214,6 +220,7 @@ export class SweetisticsClient {
return { success: false, error: 'Missing data in Sweetistics search response' }; return { success: false, error: 'Missing data in Sweetistics search response' };
} }
// biome-ignore lint/suspicious/noExplicitAny: Sweetistics responses are loosely typed
let parsed: any; let parsed: any;
try { try {
parsed = typeof dataField === 'string' ? devalueParse(dataField) : dataField; parsed = typeof dataField === 'string' ? devalueParse(dataField) : dataField;
@@ -225,6 +232,7 @@ export class SweetisticsClient {
} }
const tweetsSection = parsed?.tweets; const tweetsSection = parsed?.tweets;
// biome-ignore lint/suspicious/noExplicitAny: Sweetistics responses are loosely typed
const items: any[] = tweetsSection?.items ?? []; const items: any[] = tweetsSection?.items ?? [];
if (items.length === 0) { if (items.length === 0) {
return { success: true, tweets: [] }; return { success: true, tweets: [] };
@@ -236,8 +244,8 @@ export class SweetisticsClient {
const createdAt = const createdAt =
typeof item.createdAt === 'string' typeof item.createdAt === 'string'
? item.createdAt ? item.createdAt
: item.createdAt?.toString?.() ?? : (item.createdAt?.toString?.() ??
(Array.isArray(item.createdAt) && item.createdAt[0] === 'Date' ? item.createdAt[1] : undefined); (Array.isArray(item.createdAt) && item.createdAt[0] === 'Date' ? item.createdAt[1] : undefined));
const metrics = item.full?.metrics ?? item.full ?? item; const metrics = item.full?.metrics ?? item.full ?? item;
return { return {
@@ -281,6 +289,7 @@ export class SweetisticsClient {
return { success: false, error: `HTTP ${response.status}` }; return { success: false, error: `HTTP ${response.status}` };
} }
// biome-ignore lint/suspicious/noExplicitAny: Sweetistics responses are loosely typed
let body: any; let body: any;
try { try {
body = await response.json(); body = await response.json();
@@ -314,6 +323,7 @@ export class SweetisticsClient {
} }
const user = data && typeof data === 'object' ? data : null; 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') { if (!user || typeof (user as any).id !== 'string') {
return { success: false, error: 'Malformed user payload from Sweetistics' }; return { success: false, error: 'Malformed user payload from Sweetistics' };
} }
@@ -321,10 +331,16 @@ export class SweetisticsClient {
return { return {
success: true, success: true,
user: { user: {
// biome-ignore lint/suspicious/noExplicitAny: Sweetistics responses are loosely typed
id: String((user as any).id), 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, name: (user as any).name ?? null,
// biome-ignore lint/suspicious/noExplicitAny: Sweetistics responses are loosely typed
email: (user as any).email ?? null, email: (user as any).email ?? null,
// biome-ignore lint/suspicious/noExplicitAny: Sweetistics responses are loosely typed
profileImageUrl: (user as any).profileImageUrl ?? null, profileImageUrl: (user as any).profileImageUrl ?? null,
}, },
}; };
@@ -362,9 +378,11 @@ export class SweetisticsClient {
return { success: false, error: 'Missing data in Sweetistics response' }; return { success: false, error: 'Missing data in Sweetistics response' };
} }
// biome-ignore lint/suspicious/noExplicitAny: Sweetistics responses are loosely typed
let parsed: any; let parsed: any;
try { 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; parsed = raw;
} catch (error) { } catch (error) {
return { return {
@@ -374,6 +392,7 @@ export class SweetisticsClient {
} }
const tweetIds: string[] = Array.isArray(parsed?.tweetIds) ? parsed.tweetIds.map(String) : []; const tweetIds: string[] = Array.isArray(parsed?.tweetIds) ? parsed.tweetIds.map(String) : [];
// biome-ignore lint/suspicious/noExplicitAny: Sweetistics responses are loosely typed
const tweetsMap: Record<string, any> = parsed?.tweets ?? {}; const tweetsMap: Record<string, any> = parsed?.tweets ?? {};
if (tweetIds.length === 0 || Object.keys(tweetsMap).length === 0) { if (tweetIds.length === 0 || Object.keys(tweetsMap).length === 0) {
@@ -386,6 +405,7 @@ export class SweetisticsClient {
const tweets: SweetisticsTweet[] = filteredIds const tweets: SweetisticsTweet[] = filteredIds
.map((id) => tweetsMap[id]) .map((id) => tweetsMap[id])
.filter(Boolean) .filter(Boolean)
// biome-ignore lint/suspicious/noExplicitAny: Sweetistics responses are loosely typed
.map((item: any) => ({ .map((item: any) => ({
id: String(item.id), id: String(item.id),
text: String(item.text ?? ''), text: String(item.text ?? ''),
+3 -4
View File
@@ -237,9 +237,7 @@ export class TwitterClient {
return tweets; return tweets;
} }
private async fetchTweetDetail( private async fetchTweetDetail(tweetId: string): Promise<
tweetId: string,
): Promise<
| { | {
success: true; success: true;
data: { data: {
@@ -719,6 +717,7 @@ export class TwitterClient {
continue; continue;
} }
// biome-ignore lint/suspicious/noExplicitAny: Twitter API response is dynamic here
let data: any; let data: any;
try { try {
data = await response.json(); data = await response.json();
@@ -739,7 +738,7 @@ export class TwitterClient {
? data.name ? data.name
: typeof data?.user?.name === 'string' : typeof data?.user?.name === 'string'
? data.user.name ? data.user.name
: username ?? ''; : (username ?? '');
const userId = const userId =
typeof data?.user_id === 'string' typeof data?.user_id === 'string'
+1 -1
View File
@@ -302,7 +302,7 @@ describe('TwitterClient', () => {
beforeEach(() => { beforeEach(() => {
mockFetch = vi.fn(); mockFetch = vi.fn();
global.fetch = mockFetch as any; global.fetch = mockFetch as unknown as typeof fetch;
}); });
it('returns mapped user details when present', async () => { it('returns mapped user details when present', async () => {