chore: align biome lint config and clean warnings
This commit is contained in:
+1
-1
@@ -28,6 +28,6 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"files": {
|
"files": {
|
||||||
"ignore": ["dist/", "node_modules/", "*.d.ts"]
|
"ignore": ["dist/", "coverage/", "node_modules/", "scripts/update-query-ids.ts", "*.d.ts"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
+17
-17
@@ -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,12 +84,13 @@ const formatExample = (command: string, description: string) =>
|
|||||||
|
|
||||||
program.addHelpText(
|
program.addHelpText(
|
||||||
'afterAll',
|
'afterAll',
|
||||||
() => `\n${colors.section('Examples')}\n${[
|
() =>
|
||||||
formatExample('bird whoami', 'Show the logged-in account via GraphQL cookies'),
|
`\n${colors.section('Examples')}\n${[
|
||||||
formatExample('bird --firefox-profile default-release whoami', 'Use Firefox profile cookies'),
|
formatExample('bird whoami', 'Show the logged-in account via GraphQL cookies'),
|
||||||
formatExample('bird tweet "hello from bird"', 'Send a tweet'),
|
formatExample('bird --firefox-profile default-release whoami', 'Use Firefox profile cookies'),
|
||||||
formatExample('bird replies https://x.com/user/status/1234567890123456789', 'Check replies to a tweet'),
|
formatExample('bird tweet "hello from bird"', 'Send a tweet'),
|
||||||
].join('\n\n')}`,
|
formatExample('bird replies https://x.com/user/status/1234567890123456789', 'Check replies to a tweet'),
|
||||||
|
].join('\n\n')}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Global options for authentication
|
// Global options for authentication
|
||||||
@@ -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
@@ -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');
|
||||||
|
|||||||
@@ -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 ?? ''),
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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 () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user