feat(cli): add plain/no-emoji/no-color + help

This commit is contained in:
Peter Steinberger
2025-12-26 14:14:31 +01:00
parent 4730267c2a
commit c35e937b2a
5 changed files with 290 additions and 126 deletions
+5
View File
@@ -34,6 +34,7 @@ bird mentions --user @steipete -n 5
- `bird tweet "<text>"` — post a new tweet. - `bird tweet "<text>"` — post a new tweet.
- `bird reply <tweet-id-or-url> "<text>"` — reply to a tweet using its ID or URL. - `bird reply <tweet-id-or-url> "<text>"` — reply to a tweet using its ID or URL.
- `bird help [command]` — show help (or help for a subcommand).
- `bird read <tweet-id-or-url> [--json]` — fetch tweet content as text or JSON. - `bird read <tweet-id-or-url> [--json]` — fetch tweet content as text or JSON.
- `bird <tweet-id-or-url> [--json]` — shorthand for `read` when only a URL or ID is provided. - `bird <tweet-id-or-url> [--json]` — shorthand for `read` when only a URL or ID is provided.
- `bird replies <tweet-id-or-url> [--json]` — list replies to a tweet. - `bird replies <tweet-id-or-url> [--json]` — list replies to a tweet.
@@ -51,6 +52,9 @@ bird mentions --user @steipete -n 5
Global options: Global options:
- `--timeout <ms>`: abort requests after the given timeout (milliseconds). - `--timeout <ms>`: abort requests after the given timeout (milliseconds).
- `--plain`: stable output (no emoji, no color).
- `--no-emoji`: disable emoji output.
- `--no-color`: disable ANSI colors (or set `NO_COLOR=1`).
## Authentication (GraphQL) ## Authentication (GraphQL)
@@ -92,6 +96,7 @@ Environment shortcuts:
- `--json` prints raw tweet objects for read/replies/thread/search/mentions. - `--json` prints raw tweet objects for read/replies/thread/search/mentions.
- `read` returns full text for Notes and Articles when present. - `read` returns full text for Notes and Articles when present.
- Use `--plain` for stable, script-friendly output (no emoji, no color).
## Version ## Version
+173 -125
View File
@@ -20,19 +20,41 @@ import { resolveCredentials } from './lib/cookies.js';
import { type EngineMode, resolveEngineMode, shouldUseSweetistics } from './lib/engine.js'; import { type EngineMode, resolveEngineMode, shouldUseSweetistics } from './lib/engine.js';
import { extractTweetId } from './lib/extract-tweet-id.js'; import { extractTweetId } from './lib/extract-tweet-id.js';
import { mentionsQueryFromUserOption, normalizeHandle } from './lib/normalize-handle.js'; import { mentionsQueryFromUserOption, normalizeHandle } from './lib/normalize-handle.js';
import {
formatStatsLine,
labelPrefix,
type OutputConfig,
resolveOutputConfigFromArgv,
resolveOutputConfigFromCommander,
statusPrefix,
} from './lib/output.js';
import { SweetisticsClient } from './lib/sweetistics-client.js'; import { SweetisticsClient } from './lib/sweetistics-client.js';
import { type TweetData, TwitterClient } from './lib/twitter-client.js'; import { type TweetData, TwitterClient } from './lib/twitter-client.js';
import { getCliVersion } from './lib/version.js'; import { getCliVersion } from './lib/version.js';
const program = new Command(); const program = new Command();
const rawArgs = process.argv.slice(2);
const normalizedArgs = rawArgs[0] === '--' ? rawArgs.slice(1) : rawArgs;
const isTty = process.stdout.isTTY; const isTty = process.stdout.isTTY;
let output: OutputConfig = resolveOutputConfigFromArgv(normalizedArgs, process.env, isTty);
kleur.enabled = output.color;
const wrap = (styler: (text: string) => string) => (text: string) => (isTty ? styler(text) : text); const wrap = (styler: (text: string) => string) => (text: string) => (isTty ? styler(text) : text);
const collect = (value: string, previous: string[] = []) => { const collect = (value: string, previous: string[] = []) => {
previous.push(value); previous.push(value);
return previous; return previous;
}; };
const p = (kind: Parameters<typeof statusPrefix>[0]) => statusPrefix(kind, output);
const l = (kind: Parameters<typeof labelPrefix>[0]) => labelPrefix(kind, output);
function applyOutputFromCommand(command: Command) {
const opts = command.optsWithGlobals() as { plain?: boolean; emoji?: boolean; color?: boolean };
output = resolveOutputConfigFromCommander(opts, process.env, isTty);
kleur.enabled = output.color;
}
const colors = { const colors = {
banner: wrap((t) => kleur.bold().blue(t)), banner: wrap((t) => kleur.bold().blue(t)),
subtitle: wrap((t) => kleur.dim(t)), subtitle: wrap((t) => kleur.dim(t)),
@@ -65,7 +87,9 @@ function readConfigFile(path: string): Partial<BirdConfig> {
return parsed ?? {}; return parsed ?? {};
} catch (error) { } catch (error) {
console.error( console.error(
colors.muted(`⚠️ Failed to parse config at ${path}: ${error instanceof Error ? error.message : String(error)}`), colors.muted(
`${p('warn')}Failed to parse config at ${path}: ${error instanceof Error ? error.message : String(error)}`,
),
); );
return {}; return {};
} }
@@ -91,6 +115,7 @@ const KNOWN_COMMANDS = new Set([
'thread', 'thread',
'search', 'search',
'mentions', 'mentions',
'help',
'whoami', 'whoami',
'check', 'check',
]); ]);
@@ -135,7 +160,14 @@ program
'Engine: graphql | sweetistics | auto', 'Engine: graphql | sweetistics | auto',
process.env.BIRD_ENGINE || config.engine || 'auto', process.env.BIRD_ENGINE || config.engine || 'auto',
) )
.option('--timeout <ms>', 'Request timeout in milliseconds'); .option('--timeout <ms>', 'Request timeout in milliseconds')
.option('--plain', 'Plain output (stable, no emoji, no color)')
.option('--no-emoji', 'Disable emoji output')
.option('--no-color', 'Disable ANSI colors (or set NO_COLOR)');
program.hook('preAction', (_thisCommand, actionCommand) => {
applyOutputFromCommand(actionCommand);
});
type MediaSpec = { path: string; alt?: string; mime: string; buffer: Buffer }; type MediaSpec = { path: string; alt?: string; mime: string; buffer: Buffer };
@@ -207,15 +239,34 @@ function printTweets(
console.log(`\n@${tweet.author.username} (${tweet.author.name}):`); console.log(`\n@${tweet.author.username} (${tweet.author.name}):`);
console.log(tweet.text); console.log(tweet.text);
if (tweet.createdAt) { if (tweet.createdAt) {
console.log(`📅 ${tweet.createdAt}`); console.log(`${l('date')}${tweet.createdAt}`);
} }
console.log(`🔗 https://x.com/${tweet.author.username}/status/${tweet.id}`); console.log(`${l('url')}https://x.com/${tweet.author.username}/status/${tweet.id}`);
if (opts.showSeparator ?? true) { if (opts.showSeparator ?? true) {
console.log('─'.repeat(50)); console.log('─'.repeat(50));
} }
} }
} }
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(`${p('err')}Unknown command: ${commandName}`);
process.exitCode = 2;
return;
}
cmd.outputHelp();
});
// Tweet command // Tweet command
program program
.command('tweet') .command('tweet')
@@ -228,7 +279,7 @@ program
try { try {
media = loadMedia({ media: opts.media ?? [], alts: opts.alt ?? [] }); media = loadMedia({ media: opts.media ?? [], alts: opts.alt ?? [] });
} catch (error) { } catch (error) {
console.error(`❌ ${error instanceof Error ? error.message : String(error)}`); console.error(`${p('err')}${error instanceof Error ? error.message : String(error)}`);
process.exit(1); process.exit(1);
} }
const sweetistics = resolveSweetisticsConfig({ const sweetistics = resolveSweetisticsConfig({
@@ -240,7 +291,7 @@ program
if (useSweetistics) { if (useSweetistics) {
if (!sweetistics.apiKey) { if (!sweetistics.apiKey) {
console.error('❌ Sweetistics engine selected but no API key provided.'); console.error(`${p('err')}Sweetistics engine selected but no API key provided.`);
process.exit(1); process.exit(1);
} }
try { try {
@@ -267,18 +318,18 @@ program
} }
const result = await client.tweet(text, undefined, mediaIds); const result = await client.tweet(text, undefined, mediaIds);
if (result.success) { if (result.success) {
console.log('✅ Tweet posted via Sweetistics!'); console.log(`${p('ok')}Tweet posted via Sweetistics!`);
if (result.tweetId) { if (result.tweetId) {
console.log(`🔗 https://x.com/i/status/${result.tweetId}`); console.log(`${l('url')}https://x.com/i/status/${result.tweetId}`);
} }
return; return;
} }
console.error(`❌ Sweetistics post failed: ${result.error ?? 'Unknown error'}`); console.error(`${p('err')}Sweetistics post failed: ${result.error ?? 'Unknown error'}`);
process.exit(1); process.exit(1);
} catch (error) { } catch (error) {
console.error(`❌ Sweetistics error: ${error instanceof Error ? error.message : String(error)}`); console.error(`${p('err')}Sweetistics error: ${error instanceof Error ? error.message : String(error)}`);
if (error instanceof Error && /(^|\\b)INTERNAL_ERROR(\\b|$)|An unexpected error occurred/.test(error.message)) { if (error instanceof Error && /(^|\\b)INTERNAL_ERROR(\\b|$)|An unexpected error occurred/.test(error.message)) {
console.error('ℹ️ Hint: Sweetistics media upload likely needs Twitter OAuth with `media.write` scope.'); console.error(`${p('hint')}Sweetistics media upload likely needs Twitter OAuth with \`media.write\` scope.`);
} }
process.exit(1); process.exit(1);
} }
@@ -286,7 +337,7 @@ program
if (media.length > 0) { if (media.length > 0) {
console.error( console.error(
'❌ Media uploads are only supported via Sweetistics. Provide SWEETISTICS_API_KEY or --engine sweetistics.', `${p('err')}Media uploads are only supported via Sweetistics. Provide SWEETISTICS_API_KEY or --engine sweetistics.`,
); );
process.exit(1); process.exit(1);
} }
@@ -301,42 +352,42 @@ program
}); });
for (const warning of warnings) { for (const warning of warnings) {
console.error(`⚠️ ${warning}`); console.error(`${p('warn')}${warning}`);
} }
if (!cookies.authToken || !cookies.ct0) { if (!cookies.authToken || !cookies.ct0) {
console.error('❌ Missing required credentials'); console.error(`${p('err')}Missing required credentials`);
process.exit(1); process.exit(1);
} }
if (cookies.source) { if (cookies.source) {
console.error(`📍 Using credentials from: ${cookies.source}`); console.error(`${l('source')}${cookies.source}`);
} }
const client = new TwitterClient({ cookies, timeoutMs }); const client = new TwitterClient({ cookies, timeoutMs });
const result = await client.tweet(text); const result = await client.tweet(text);
if (result.success) { if (result.success) {
console.log('✅ Tweet posted successfully!'); console.log(`${p('ok')}Tweet posted successfully!`);
console.log(`🔗 https://x.com/i/status/${result.tweetId}`); console.log(`${l('url')}https://x.com/i/status/${result.tweetId}`);
} else if (sweetistics.apiKey) { } else if (sweetistics.apiKey) {
console.error(`⚠️ GraphQL tweet failed (${result.error}); trying Sweetistics fallback...`); console.error(`${p('warn')}GraphQL tweet failed (${result.error}); trying Sweetistics fallback...`);
const fallback = await new SweetisticsClient({ const fallback = await new SweetisticsClient({
baseUrl: sweetistics.baseUrl, baseUrl: sweetistics.baseUrl,
apiKey: sweetistics.apiKey, apiKey: sweetistics.apiKey,
timeoutMs, timeoutMs,
}).tweet(text); }).tweet(text);
if (fallback.success) { if (fallback.success) {
console.log('✅ Tweet posted via Sweetistics (fallback)!'); console.log(`${p('ok')}Tweet posted via Sweetistics (fallback)!`);
if (fallback.tweetId) { if (fallback.tweetId) {
console.log(`🔗 https://x.com/i/status/${fallback.tweetId}`); console.log(`${l('url')}https://x.com/i/status/${fallback.tweetId}`);
} }
} else { } else {
console.error(`❌ Failed to post tweet: ${result.error} | Sweetistics fallback: ${fallback.error}`); console.error(`${p('err')}Failed to post tweet: ${result.error} | Sweetistics fallback: ${fallback.error}`);
process.exit(1); process.exit(1);
} }
} else { } else {
console.error(`❌ Failed to post tweet: ${result.error}`); console.error(`${p('err')}Failed to post tweet: ${result.error}`);
process.exit(1); process.exit(1);
} }
}); });
@@ -354,7 +405,7 @@ program
try { try {
media = loadMedia({ media: opts.media ?? [], alts: opts.alt ?? [] }); media = loadMedia({ media: opts.media ?? [], alts: opts.alt ?? [] });
} catch (error) { } catch (error) {
console.error(`❌ ${error instanceof Error ? error.message : String(error)}`); console.error(`${p('err')}${error instanceof Error ? error.message : String(error)}`);
process.exit(1); process.exit(1);
} }
const sweetistics = resolveSweetisticsConfig({ const sweetistics = resolveSweetisticsConfig({
@@ -367,7 +418,7 @@ program
if (useSweetistics) { if (useSweetistics) {
if (!sweetistics.apiKey) { if (!sweetistics.apiKey) {
console.error('❌ Sweetistics engine selected but no API key provided.'); console.error(`${p('err')}Sweetistics engine selected but no API key provided.`);
process.exit(1); process.exit(1);
} }
try { try {
@@ -394,18 +445,18 @@ program
} }
const result = await client.tweet(text, tweetId, mediaIds); const result = await client.tweet(text, tweetId, mediaIds);
if (result.success) { if (result.success) {
console.log('✅ Reply posted via Sweetistics!'); console.log(`${p('ok')}Reply posted via Sweetistics!`);
if (result.tweetId) { if (result.tweetId) {
console.log(`🔗 https://x.com/i/status/${result.tweetId}`); console.log(`${l('url')}https://x.com/i/status/${result.tweetId}`);
} }
return; return;
} }
console.error(`❌ Sweetistics reply failed: ${result.error ?? 'Unknown error'}`); console.error(`${p('err')}Sweetistics reply failed: ${result.error ?? 'Unknown error'}`);
process.exit(1); process.exit(1);
} catch (error) { } catch (error) {
console.error(`❌ Sweetistics error: ${error instanceof Error ? error.message : String(error)}`); console.error(`${p('err')}Sweetistics error: ${error instanceof Error ? error.message : String(error)}`);
if (error instanceof Error && /(^|\\b)INTERNAL_ERROR(\\b|$)|An unexpected error occurred/.test(error.message)) { if (error instanceof Error && /(^|\\b)INTERNAL_ERROR(\\b|$)|An unexpected error occurred/.test(error.message)) {
console.error('ℹ️ Hint: Sweetistics media upload likely needs Twitter OAuth with `media.write` scope.'); console.error(`${p('hint')}Sweetistics media upload likely needs Twitter OAuth with \`media.write\` scope.`);
} }
process.exit(1); process.exit(1);
} }
@@ -413,7 +464,7 @@ program
if (media.length > 0) { if (media.length > 0) {
console.error( console.error(
'❌ Media uploads are only supported via Sweetistics. Provide SWEETISTICS_API_KEY or --engine sweetistics.', `${p('err')}Media uploads are only supported via Sweetistics. Provide SWEETISTICS_API_KEY or --engine sweetistics.`,
); );
process.exit(1); process.exit(1);
} }
@@ -428,44 +479,44 @@ program
}); });
for (const warning of warnings) { for (const warning of warnings) {
console.error(`⚠️ ${warning}`); console.error(`${p('warn')}${warning}`);
} }
if (!cookies.authToken || !cookies.ct0) { if (!cookies.authToken || !cookies.ct0) {
console.error('❌ Missing required credentials'); console.error(`${p('err')}Missing required credentials`);
process.exit(1); process.exit(1);
} }
if (cookies.source) { if (cookies.source) {
console.error(`📍 Using credentials from: ${cookies.source}`); console.error(`${l('source')}${cookies.source}`);
} }
console.error(`📝 Replying to tweet: ${tweetId}`); console.error(`${p('info')}Replying to tweet: ${tweetId}`);
const client = new TwitterClient({ cookies, timeoutMs }); const client = new TwitterClient({ cookies, timeoutMs });
const result = await client.reply(text, tweetId); const result = await client.reply(text, tweetId);
if (result.success) { if (result.success) {
console.log('✅ Reply posted successfully!'); console.log(`${p('ok')}Reply posted successfully!`);
console.log(`🔗 https://x.com/i/status/${result.tweetId}`); console.log(`${l('url')}https://x.com/i/status/${result.tweetId}`);
} else if (sweetistics.apiKey) { } else if (sweetistics.apiKey) {
console.error(`⚠️ GraphQL reply failed (${result.error}); trying Sweetistics fallback...`); console.error(`${p('warn')}GraphQL reply failed (${result.error}); trying Sweetistics fallback...`);
const fallback = await new SweetisticsClient({ const fallback = await new SweetisticsClient({
baseUrl: sweetistics.baseUrl, baseUrl: sweetistics.baseUrl,
apiKey: sweetistics.apiKey, apiKey: sweetistics.apiKey,
timeoutMs, timeoutMs,
}).tweet(text, tweetId); }).tweet(text, tweetId);
if (fallback.success) { if (fallback.success) {
console.log('✅ Reply posted via Sweetistics (fallback)!'); console.log(`${p('ok')}Reply posted via Sweetistics (fallback)!`);
if (fallback.tweetId) { if (fallback.tweetId) {
console.log(`🔗 https://x.com/i/status/${fallback.tweetId}`); console.log(`${l('url')}https://x.com/i/status/${fallback.tweetId}`);
} }
} else { } else {
console.error(`❌ Failed to post reply: ${result.error} | Sweetistics fallback: ${fallback.error}`); console.error(`${p('err')}Failed to post reply: ${result.error} | Sweetistics fallback: ${fallback.error}`);
process.exit(1); process.exit(1);
} }
} else { } else {
console.error(`❌ Failed to post reply: ${result.error}`); console.error(`${p('err')}Failed to post reply: ${result.error}`);
process.exit(1); process.exit(1);
} }
}); });
@@ -489,7 +540,7 @@ program
const tweetId = extractTweetId(tweetIdOrUrl); const tweetId = extractTweetId(tweetIdOrUrl);
if (useSweetistics) { if (useSweetistics) {
if (!sweetistics.apiKey) { if (!sweetistics.apiKey) {
console.error('❌ Sweetistics engine selected but no API key provided.'); console.error(`${p('err')}Sweetistics engine selected but no API key provided.`);
process.exit(1); process.exit(1);
} }
const client = new SweetisticsClient({ const client = new SweetisticsClient({
@@ -505,15 +556,13 @@ program
console.log(`@${result.tweet.author.username} (${result.tweet.author.name}):`); console.log(`@${result.tweet.author.username} (${result.tweet.author.name}):`);
console.log(result.tweet.text); console.log(result.tweet.text);
if (result.tweet.createdAt) { if (result.tweet.createdAt) {
console.log(`\n📅 ${result.tweet.createdAt}`); console.log(`\n${l('date')}${result.tweet.createdAt}`);
} }
console.log( console.log(formatStatsLine(result.tweet, output));
`❤️ ${result.tweet.likeCount ?? 0} 🔁 ${result.tweet.retweetCount ?? 0} 💬 ${result.tweet.replyCount ?? 0}`,
);
} }
return; return;
} }
console.error(`❌ Failed to read tweet via Sweetistics: ${result.error ?? 'Unknown error'}`); console.error(`${p('err')}Failed to read tweet via Sweetistics: ${result.error ?? 'Unknown error'}`);
process.exit(1); process.exit(1);
} }
@@ -527,11 +576,11 @@ program
}); });
for (const warning of warnings) { for (const warning of warnings) {
console.error(`⚠️ ${warning}`); console.error(`${p('warn')}${warning}`);
} }
if (!cookies.authToken || !cookies.ct0) { if (!cookies.authToken || !cookies.ct0) {
console.error('❌ Missing required credentials'); console.error(`${p('err')}Missing required credentials`);
process.exit(1); process.exit(1);
} }
@@ -545,14 +594,12 @@ program
console.log(`@${result.tweet.author.username} (${result.tweet.author.name}):`); console.log(`@${result.tweet.author.username} (${result.tweet.author.name}):`);
console.log(result.tweet.text); console.log(result.tweet.text);
if (result.tweet.createdAt) { if (result.tweet.createdAt) {
console.log(`\n📅 ${result.tweet.createdAt}`); console.log(`\n${l('date')}${result.tweet.createdAt}`);
} }
console.log( console.log(formatStatsLine(result.tweet, output));
`❤️ ${result.tweet.likeCount ?? 0} 🔁 ${result.tweet.retweetCount ?? 0} 💬 ${result.tweet.replyCount ?? 0}`,
);
} }
} else if (sweetistics.apiKey) { } else if (sweetistics.apiKey) {
console.error(`⚠️ GraphQL read failed (${result.error}); trying Sweetistics fallback...`); console.error(`${p('warn')}GraphQL read failed (${result.error}); trying Sweetistics fallback...`);
const fallback = await new SweetisticsClient({ const fallback = await new SweetisticsClient({
baseUrl: sweetistics.baseUrl, baseUrl: sweetistics.baseUrl,
apiKey: sweetistics.apiKey, apiKey: sweetistics.apiKey,
@@ -565,18 +612,16 @@ program
console.log(`@${fallback.tweet.author.username} (${fallback.tweet.author.name}):`); console.log(`@${fallback.tweet.author.username} (${fallback.tweet.author.name}):`);
console.log(fallback.tweet.text); console.log(fallback.tweet.text);
if (fallback.tweet.createdAt) { if (fallback.tweet.createdAt) {
console.log(`\n📅 ${fallback.tweet.createdAt}`); console.log(`\n${l('date')}${fallback.tweet.createdAt}`);
} }
console.log( console.log(formatStatsLine(fallback.tweet, output));
`❤️ ${fallback.tweet.likeCount ?? 0} 🔁 ${fallback.tweet.retweetCount ?? 0} 💬 ${fallback.tweet.replyCount ?? 0}`,
);
} }
} else { } else {
console.error(`❌ Failed to read tweet: ${result.error} | Sweetistics fallback: ${fallback.error}`); console.error(`${p('err')}Failed to read tweet: ${result.error} | Sweetistics fallback: ${fallback.error}`);
process.exit(1); process.exit(1);
} }
} else { } else {
console.error(`❌ Failed to read tweet: ${result.error}`); console.error(`${p('err')}Failed to read tweet: ${result.error}`);
process.exit(1); process.exit(1);
} }
}); });
@@ -599,7 +644,7 @@ program
const tweetId = extractTweetId(tweetIdOrUrl); const tweetId = extractTweetId(tweetIdOrUrl);
if (useSweetistics) { if (useSweetistics) {
if (!sweetistics.apiKey) { if (!sweetistics.apiKey) {
console.error('❌ Sweetistics engine selected but no API key provided.'); console.error(`${p('err')}Sweetistics engine selected but no API key provided.`);
process.exit(1); process.exit(1);
} }
const client = new SweetisticsClient({ const client = new SweetisticsClient({
@@ -612,7 +657,7 @@ program
printTweets(result.tweets, { json: cmdOpts.json, emptyMessage: 'No replies found.' }); printTweets(result.tweets, { json: cmdOpts.json, emptyMessage: 'No replies found.' });
return; return;
} }
console.error(`❌ Failed to fetch replies via Sweetistics: ${result.error}`); console.error(`${p('err')}Failed to fetch replies via Sweetistics: ${result.error}`);
process.exit(1); process.exit(1);
} }
@@ -624,11 +669,11 @@ program
}); });
for (const warning of warnings) { for (const warning of warnings) {
console.error(`⚠️ ${warning}`); console.error(`${p('warn')}${warning}`);
} }
if (!cookies.authToken || !cookies.ct0) { if (!cookies.authToken || !cookies.ct0) {
console.error('❌ Missing required credentials'); console.error(`${p('err')}Missing required credentials`);
process.exit(1); process.exit(1);
} }
@@ -638,7 +683,7 @@ program
if (result.success && result.tweets) { if (result.success && result.tweets) {
printTweets(result.tweets, { json: cmdOpts.json, emptyMessage: 'No replies found.' }); printTweets(result.tweets, { json: cmdOpts.json, emptyMessage: 'No replies found.' });
} else if (sweetistics.apiKey) { } else if (sweetistics.apiKey) {
console.error(`⚠️ GraphQL replies failed (${result.error}); trying Sweetistics fallback...`); console.error(`${p('warn')}GraphQL replies failed (${result.error}); trying Sweetistics fallback...`);
const fallback = await new SweetisticsClient({ const fallback = await new SweetisticsClient({
baseUrl: sweetistics.baseUrl, baseUrl: sweetistics.baseUrl,
apiKey: sweetistics.apiKey, apiKey: sweetistics.apiKey,
@@ -647,11 +692,11 @@ program
if (fallback.success && fallback.tweets) { if (fallback.success && fallback.tweets) {
printTweets(fallback.tweets, { json: cmdOpts.json, emptyMessage: 'No replies found.' }); printTweets(fallback.tweets, { json: cmdOpts.json, emptyMessage: 'No replies found.' });
} else { } else {
console.error(`❌ Failed to fetch replies: ${result.error} | Sweetistics fallback: ${fallback.error}`); console.error(`${p('err')}Failed to fetch replies: ${result.error} | Sweetistics fallback: ${fallback.error}`);
process.exit(1); process.exit(1);
} }
} else { } else {
console.error(`❌ Failed to fetch replies: ${result.error}`); console.error(`${p('err')}Failed to fetch replies: ${result.error}`);
process.exit(1); process.exit(1);
} }
}); });
@@ -674,7 +719,7 @@ program
const tweetId = extractTweetId(tweetIdOrUrl); const tweetId = extractTweetId(tweetIdOrUrl);
if (useSweetistics) { if (useSweetistics) {
if (!sweetistics.apiKey) { if (!sweetistics.apiKey) {
console.error('❌ Sweetistics engine selected but no API key provided.'); console.error(`${p('err')}Sweetistics engine selected but no API key provided.`);
process.exit(1); process.exit(1);
} }
const client = new SweetisticsClient({ const client = new SweetisticsClient({
@@ -687,7 +732,7 @@ program
printTweets(result.tweets, { json: cmdOpts.json, emptyMessage: 'No thread tweets found.' }); printTweets(result.tweets, { json: cmdOpts.json, emptyMessage: 'No thread tweets found.' });
return; return;
} }
console.error(`❌ Failed to fetch thread via Sweetistics: ${result.error}`); console.error(`${p('err')}Failed to fetch thread via Sweetistics: ${result.error}`);
process.exit(1); process.exit(1);
} }
@@ -699,11 +744,11 @@ program
}); });
for (const warning of warnings) { for (const warning of warnings) {
console.error(`⚠️ ${warning}`); console.error(`${p('warn')}${warning}`);
} }
if (!cookies.authToken || !cookies.ct0) { if (!cookies.authToken || !cookies.ct0) {
console.error('❌ Missing required credentials'); console.error(`${p('err')}Missing required credentials`);
process.exit(1); process.exit(1);
} }
@@ -713,7 +758,7 @@ program
if (result.success && result.tweets) { if (result.success && result.tweets) {
printTweets(result.tweets, { json: cmdOpts.json, emptyMessage: 'No thread tweets found.' }); printTweets(result.tweets, { json: cmdOpts.json, emptyMessage: 'No thread tweets found.' });
} else if (sweetistics.apiKey) { } else if (sweetistics.apiKey) {
console.error(`⚠️ GraphQL thread failed (${result.error}); trying Sweetistics fallback...`); console.error(`${p('warn')}GraphQL thread failed (${result.error}); trying Sweetistics fallback...`);
const fallback = await new SweetisticsClient({ const fallback = await new SweetisticsClient({
baseUrl: sweetistics.baseUrl, baseUrl: sweetistics.baseUrl,
apiKey: sweetistics.apiKey, apiKey: sweetistics.apiKey,
@@ -722,11 +767,11 @@ program
if (fallback.success && fallback.tweets) { if (fallback.success && fallback.tweets) {
printTweets(fallback.tweets, { json: cmdOpts.json, emptyMessage: 'No thread tweets found.' }); printTweets(fallback.tweets, { json: cmdOpts.json, emptyMessage: 'No thread tweets found.' });
} else { } else {
console.error(`❌ Failed to fetch thread: ${result.error} | Sweetistics fallback: ${fallback.error}`); console.error(`${p('err')}Failed to fetch thread: ${result.error} | Sweetistics fallback: ${fallback.error}`);
process.exit(1); process.exit(1);
} }
} else { } else {
console.error(`❌ Failed to fetch thread: ${result.error}`); console.error(`${p('err')}Failed to fetch thread: ${result.error}`);
process.exit(1); process.exit(1);
} }
}); });
@@ -751,7 +796,7 @@ program
if (useSweetistics) { if (useSweetistics) {
if (!sweetistics.apiKey) { if (!sweetistics.apiKey) {
console.error('❌ Sweetistics engine selected but no API key provided.'); console.error(`${p('err')}Sweetistics engine selected but no API key provided.`);
process.exit(1); process.exit(1);
} }
const client = new SweetisticsClient({ const client = new SweetisticsClient({
@@ -764,7 +809,7 @@ program
printTweets(result.tweets, { json: cmdOpts.json, emptyMessage: 'No tweets found.' }); printTweets(result.tweets, { json: cmdOpts.json, emptyMessage: 'No tweets found.' });
return; return;
} }
console.error(`❌ Search failed via Sweetistics: ${result.error}`); console.error(`${p('err')}Search failed via Sweetistics: ${result.error}`);
process.exit(1); process.exit(1);
} }
@@ -776,11 +821,11 @@ program
}); });
for (const warning of warnings) { for (const warning of warnings) {
console.error(`⚠️ ${warning}`); console.error(`${p('warn')}${warning}`);
} }
if (!cookies.authToken || !cookies.ct0) { if (!cookies.authToken || !cookies.ct0) {
console.error('❌ Missing required credentials'); console.error(`${p('err')}Missing required credentials`);
process.exit(1); process.exit(1);
} }
@@ -790,7 +835,7 @@ program
if (result.success && result.tweets) { if (result.success && result.tweets) {
printTweets(result.tweets, { json: cmdOpts.json, emptyMessage: 'No tweets found.' }); printTweets(result.tweets, { json: cmdOpts.json, emptyMessage: 'No tweets found.' });
} else if (sweetistics.apiKey) { } else if (sweetistics.apiKey) {
console.error(`⚠️ GraphQL search failed (${result.error}); trying Sweetistics fallback...`); console.error(`${p('warn')}GraphQL search failed (${result.error}); trying Sweetistics fallback...`);
const fallback = await new SweetisticsClient({ const fallback = await new SweetisticsClient({
baseUrl: sweetistics.baseUrl, baseUrl: sweetistics.baseUrl,
apiKey: sweetistics.apiKey, apiKey: sweetistics.apiKey,
@@ -799,11 +844,11 @@ program
if (fallback.success && fallback.tweets) { if (fallback.success && fallback.tweets) {
printTweets(fallback.tweets, { json: cmdOpts.json, emptyMessage: 'No tweets found.' }); printTweets(fallback.tweets, { json: cmdOpts.json, emptyMessage: 'No tweets found.' });
} else { } else {
console.error(`❌ Search failed: ${result.error} | Sweetistics fallback: ${fallback.error}`); console.error(`${p('err')}Search failed: ${result.error} | Sweetistics fallback: ${fallback.error}`);
process.exit(1); process.exit(1);
} }
} else { } else {
console.error(`❌ Search failed: ${result.error}`); console.error(`${p('err')}Search failed: ${result.error}`);
process.exit(1); process.exit(1);
} }
}); });
@@ -825,7 +870,7 @@ program
const fromUserOpt = mentionsQueryFromUserOption(cmdOpts.user); const fromUserOpt = mentionsQueryFromUserOption(cmdOpts.user);
if (fromUserOpt.error) { if (fromUserOpt.error) {
console.error(`❌ ${fromUserOpt.error}`); console.error(`${p('err')}${fromUserOpt.error}`);
process.exit(2); process.exit(2);
} }
@@ -833,7 +878,7 @@ program
if (useSweetistics) { if (useSweetistics) {
if (!sweetistics.apiKey) { if (!sweetistics.apiKey) {
console.error('❌ Sweetistics engine selected but no API key provided.'); console.error(`${p('err')}Sweetistics engine selected but no API key provided.`);
process.exit(1); process.exit(1);
} }
const client = new SweetisticsClient({ const client = new SweetisticsClient({
@@ -846,7 +891,9 @@ program
const who = await client.getCurrentUser(); const who = await client.getCurrentUser();
const handle = normalizeHandle(who.user?.username); const handle = normalizeHandle(who.user?.username);
if (!handle) { if (!handle) {
console.error(`❌ Could not determine current user (${who.error ?? 'Unknown error'}). Use --user <handle>.`); console.error(
`${p('err')}Could not determine current user (${who.error ?? 'Unknown error'}). Use --user <handle>.`,
);
process.exit(1); process.exit(1);
} }
query = `@${handle}`; query = `@${handle}`;
@@ -857,7 +904,7 @@ program
printTweets(result.tweets, { json: cmdOpts.json, emptyMessage: 'No mentions found.' }); printTweets(result.tweets, { json: cmdOpts.json, emptyMessage: 'No mentions found.' });
return; return;
} }
console.error(`❌ Failed to fetch mentions via Sweetistics: ${result.error}`); console.error(`${p('err')}Failed to fetch mentions via Sweetistics: ${result.error}`);
process.exit(1); process.exit(1);
} }
@@ -868,11 +915,11 @@ program
}); });
for (const warning of warnings) { for (const warning of warnings) {
console.error(`⚠️ ${warning}`); console.error(`${p('warn')}${warning}`);
} }
if (!cookies.authToken || !cookies.ct0) { if (!cookies.authToken || !cookies.ct0) {
console.error('❌ Missing required credentials'); console.error(`${p('err')}Missing required credentials`);
process.exit(1); process.exit(1);
} }
@@ -884,7 +931,7 @@ program
if (!handle) { if (!handle) {
if (sweetistics.apiKey) { if (sweetistics.apiKey) {
console.error( console.error(
`⚠️ Could not determine current user (${who.error ?? 'Unknown error'}); trying Sweetistics fallback...`, `${p('warn')}Could not determine current user (${who.error ?? 'Unknown error'}); trying Sweetistics fallback...`,
); );
const fallbackWho = await new SweetisticsClient({ const fallbackWho = await new SweetisticsClient({
baseUrl: sweetistics.baseUrl, baseUrl: sweetistics.baseUrl,
@@ -893,12 +940,14 @@ program
}).getCurrentUser(); }).getCurrentUser();
const fallbackHandle = normalizeHandle(fallbackWho.user?.username); const fallbackHandle = normalizeHandle(fallbackWho.user?.username);
if (!fallbackHandle) { if (!fallbackHandle) {
console.error(`❌ Could not determine current user. Use --user <handle>.`); console.error(`${p('err')}Could not determine current user. Use --user <handle>.`);
process.exit(1); process.exit(1);
} }
query = `@${fallbackHandle}`; query = `@${fallbackHandle}`;
} else { } else {
console.error(`❌ Could not determine current user (${who.error ?? 'Unknown error'}). Use --user <handle>.`); console.error(
`${p('err')}Could not determine current user (${who.error ?? 'Unknown error'}). Use --user <handle>.`,
);
process.exit(1); process.exit(1);
} }
} else { } else {
@@ -911,7 +960,7 @@ program
if (result.success && result.tweets) { if (result.success && result.tweets) {
printTweets(result.tweets, { json: cmdOpts.json, emptyMessage: 'No mentions found.' }); printTweets(result.tweets, { json: cmdOpts.json, emptyMessage: 'No mentions found.' });
} else if (sweetistics.apiKey) { } else if (sweetistics.apiKey) {
console.error(`⚠️ GraphQL mentions failed (${result.error}); trying Sweetistics fallback...`); console.error(`${p('warn')}GraphQL mentions failed (${result.error}); trying Sweetistics fallback...`);
const fallback = await new SweetisticsClient({ const fallback = await new SweetisticsClient({
baseUrl: sweetistics.baseUrl, baseUrl: sweetistics.baseUrl,
apiKey: sweetistics.apiKey, apiKey: sweetistics.apiKey,
@@ -920,11 +969,11 @@ program
if (fallback.success && fallback.tweets) { if (fallback.success && fallback.tweets) {
printTweets(fallback.tweets, { json: cmdOpts.json, emptyMessage: 'No mentions found.' }); printTweets(fallback.tweets, { json: cmdOpts.json, emptyMessage: 'No mentions found.' });
} else { } else {
console.error(`❌ Failed to fetch mentions: ${result.error} | Sweetistics fallback: ${fallback.error}`); console.error(`${p('err')}Failed to fetch mentions: ${result.error} | Sweetistics fallback: ${fallback.error}`);
process.exit(1); process.exit(1);
} }
} else { } else {
console.error(`❌ Failed to fetch mentions: ${result.error}`); console.error(`${p('err')}Failed to fetch mentions: ${result.error}`);
process.exit(1); process.exit(1);
} }
}); });
@@ -945,7 +994,7 @@ program
const resolvedEngine = useSweetistics ? 'sweetistics' : 'graphql'; const resolvedEngine = useSweetistics ? 'sweetistics' : 'graphql';
if (useSweetistics) { if (useSweetistics) {
if (!sweetistics.apiKey) { if (!sweetistics.apiKey) {
console.error('❌ Sweetistics engine selected but no API key provided.'); console.error(`${p('err')}Sweetistics engine selected but no API key provided.`);
process.exit(1); process.exit(1);
} }
@@ -959,17 +1008,17 @@ program
if (result.success && result.user) { if (result.success && result.user) {
const handle = result.user.username ? `@${result.user.username}` : '(no handle)'; const handle = result.user.username ? `@${result.user.username}` : '(no handle)';
const name = result.user.name || handle; const name = result.user.name || handle;
console.log(`🙋 Logged in via Sweetistics as ${handle} (${name})`); console.log(`${l('user')}${handle} (${name})`);
console.log(`🪪 User ID: ${result.user.id}`); console.log(`${l('userId')}${result.user.id}`);
if (result.user.email) { if (result.user.email) {
console.log(`📧 ${result.user.email}`); console.log(`${l('email')}${result.user.email}`);
} }
console.log(`⚙️ Engine: ${resolvedEngine}`); console.log(`${l('engine')}${resolvedEngine}`);
console.log('🔑 Credentials: Sweetistics API key'); console.log(`${l('credentials')}Sweetistics API key`);
return; return;
} }
console.error(`❌ Failed to determine Sweetistics user: ${result.error ?? 'Unknown error'}`); console.error(`${p('err')}Failed to determine Sweetistics user: ${result.error ?? 'Unknown error'}`);
process.exit(1); process.exit(1);
} }
@@ -983,16 +1032,16 @@ program
}); });
for (const warning of warnings) { for (const warning of warnings) {
console.error(`⚠️ ${warning}`); console.error(`${p('warn')}${warning}`);
} }
if (!cookies.authToken || !cookies.ct0) { if (!cookies.authToken || !cookies.ct0) {
console.error('❌ Missing required credentials'); console.error(`${p('err')}Missing required credentials`);
process.exit(1); process.exit(1);
} }
if (cookies.source) { if (cookies.source) {
console.error(`📍 Using credentials from: ${cookies.source}`); console.error(`${l('source')}${cookies.source}`);
} }
const client = new TwitterClient({ cookies, timeoutMs }); const client = new TwitterClient({ cookies, timeoutMs });
@@ -1001,10 +1050,10 @@ program
const credentialSource = cookies.source ?? 'env/auto-detected cookies'; const credentialSource = cookies.source ?? 'env/auto-detected cookies';
if (result.success && result.user) { if (result.success && result.user) {
console.log(`🙋 Logged in as @${result.user.username} (${result.user.name})`); console.log(`${l('user')}@${result.user.username} (${result.user.name})`);
console.log(`🪪 User ID: ${result.user.id}`); console.log(`${l('userId')}${result.user.id}`);
console.log(`⚙️ Engine: ${resolvedEngine}`); console.log(`${l('engine')}${resolvedEngine}`);
console.log(`🔑 Credentials: ${credentialSource}`); console.log(`${l('credentials')}${credentialSource}`);
} else { } else {
// Fallback: try Sweetistics if available // Fallback: try Sweetistics if available
if (sweetistics.apiKey) { if (sweetistics.apiKey) {
@@ -1016,17 +1065,17 @@ program
if (fallback.success && fallback.user) { if (fallback.success && fallback.user) {
const handle = fallback.user.username ? `@${fallback.user.username}` : '(no handle)'; const handle = fallback.user.username ? `@${fallback.user.username}` : '(no handle)';
const name = fallback.user.name || handle; const name = fallback.user.name || handle;
console.log(`🙋 Logged in via Sweetistics as ${handle} (${name})`); console.log(`${l('user')}${handle} (${name})`);
console.log(`🪪 User ID: ${fallback.user.id}`); console.log(`${l('userId')}${fallback.user.id}`);
if (fallback.user.email) { if (fallback.user.email) {
console.log(`📧 ${fallback.user.email}`); console.log(`${l('email')}${fallback.user.email}`);
} }
console.log('⚙️ Engine: sweetistics (fallback)'); console.log(`${l('engine')}sweetistics (fallback)`);
console.log('🔑 Credentials: Sweetistics API key'); console.log(`${l('credentials')}Sweetistics API key`);
return; return;
} }
} }
console.error(`❌ Failed to determine current user: ${result.error ?? 'Unknown error'}`); console.error(`${p('err')}Failed to determine current user: ${result.error ?? 'Unknown error'}`);
process.exit(1); process.exit(1);
} }
}); });
@@ -1043,36 +1092,36 @@ program
chromeProfile: opts.chromeProfile, chromeProfile: opts.chromeProfile,
}); });
console.log('🔍 Credential Check'); console.log(`${p('info')}Credential check`);
console.log('─'.repeat(40)); console.log('─'.repeat(40));
if (cookies.authToken) { if (cookies.authToken) {
console.log(`✅ auth_token: ${cookies.authToken.slice(0, 10)}...`); console.log(`${p('ok')}auth_token: ${cookies.authToken.slice(0, 10)}...`);
} else { } else {
console.log('❌ auth_token: not found'); console.log(`${p('err')}auth_token: not found`);
} }
if (cookies.ct0) { if (cookies.ct0) {
console.log(`✅ ct0: ${cookies.ct0.slice(0, 10)}...`); console.log(`${p('ok')}ct0: ${cookies.ct0.slice(0, 10)}...`);
} else { } else {
console.log('❌ ct0: not found'); console.log(`${p('err')}ct0: not found`);
} }
if (cookies.source) { if (cookies.source) {
console.log(`📍 Source: ${cookies.source}`); console.log(`${l('source')}${cookies.source}`);
} }
if (warnings.length > 0) { if (warnings.length > 0) {
console.log('\n⚠️ Warnings:'); console.log(`\n${p('warn')}Warnings:`);
for (const warning of warnings) { for (const warning of warnings) {
console.log(` - ${warning}`); console.log(` - ${warning}`);
} }
} }
if (cookies.authToken && cookies.ct0) { if (cookies.authToken && cookies.ct0) {
console.log('\n✅ Ready to tweet!'); console.log(`\n${p('ok')}Ready to tweet!`);
} else { } else {
console.log('\n❌ Missing credentials. Options:'); console.log(`\n${p('err')}Missing credentials. Options:`);
console.log(' 1. Login to x.com in Chrome'); console.log(' 1. Login to x.com in Chrome');
console.log(' 2. Set AUTH_TOKEN and CT0 environment variables'); console.log(' 2. Set AUTH_TOKEN and CT0 environment variables');
console.log(' 3. Use --auth-token and --ct0 flags'); console.log(' 3. Use --auth-token and --ct0 flags');
@@ -1080,8 +1129,7 @@ program
} }
}); });
const rawArgs = process.argv.slice(2); const { argv, showHelp } = resolveCliInvocation(normalizedArgs, KNOWN_COMMANDS);
const { argv, showHelp } = resolveCliInvocation(rawArgs, KNOWN_COMMANDS);
if (showHelp) { if (showHelp) {
program.outputHelp(); program.outputHelp();
@@ -1091,5 +1139,5 @@ if (showHelp) {
if (argv) { if (argv) {
program.parse(argv); program.parse(argv);
} else { } else {
program.parse(); program.parse(['node', 'bird', ...normalizedArgs]);
} }
+78
View File
@@ -0,0 +1,78 @@
export type OutputConfig = {
plain: boolean;
emoji: boolean;
color: boolean;
};
export type StatusKind = 'ok' | 'warn' | 'err' | 'info' | 'hint';
export type LabelKind = 'url' | 'date' | 'source' | 'engine' | 'credentials' | 'user' | 'userId' | 'email';
const STATUS: Record<StatusKind, { emoji: string; text: string; plain: string }> = {
ok: { emoji: '✅', text: 'OK:', plain: '[ok]' },
warn: { emoji: '⚠️', text: 'Warning:', plain: '[warn]' },
err: { emoji: '❌', text: 'Error:', plain: '[err]' },
info: { emoji: 'ℹ️', text: 'Info:', plain: '[info]' },
hint: { emoji: 'ℹ️', text: 'Hint:', plain: '[hint]' },
};
const LABELS: Record<LabelKind, { emoji: string; text: string; plain: string }> = {
url: { emoji: '🔗', text: 'URL:', plain: 'url:' },
date: { emoji: '📅', text: 'Date:', plain: 'date:' },
source: { emoji: '📍', text: 'Source:', plain: 'source:' },
engine: { emoji: '⚙️', text: 'Engine:', plain: 'engine:' },
credentials: { emoji: '🔑', text: 'Credentials:', plain: 'credentials:' },
user: { emoji: '🙋', text: 'User:', plain: 'user:' },
userId: { emoji: '🪪', text: 'User ID:', plain: 'user_id:' },
email: { emoji: '📧', text: 'Email:', plain: 'email:' },
};
export function resolveOutputConfigFromArgv(argv: string[], env: NodeJS.ProcessEnv, isTty: boolean): OutputConfig {
const hasNoColorEnv = Object.hasOwn(env, 'NO_COLOR') || env.TERM === 'dumb';
const defaultColor = isTty && !hasNoColorEnv;
const plain = argv.includes('--plain');
const emoji = !plain && !argv.includes('--no-emoji');
const color = !plain && !argv.includes('--no-color') && defaultColor;
return { plain, emoji, color };
}
export function resolveOutputConfigFromCommander(
opts: { plain?: boolean; emoji?: boolean; color?: boolean },
env: NodeJS.ProcessEnv,
isTty: boolean,
): OutputConfig {
const hasNoColorEnv = Object.hasOwn(env, 'NO_COLOR') || env.TERM === 'dumb';
const defaultColor = isTty && !hasNoColorEnv;
const plain = Boolean(opts.plain);
const emoji = !plain && (opts.emoji ?? true);
const color = !plain && (opts.color ?? true) && defaultColor;
return { plain, emoji, color };
}
export function statusPrefix(kind: StatusKind, cfg: OutputConfig): string {
if (cfg.plain) return `${STATUS[kind].plain} `;
if (cfg.emoji) return `${STATUS[kind].emoji} `;
return `${STATUS[kind].text} `;
}
export function labelPrefix(kind: LabelKind, cfg: OutputConfig): string {
if (cfg.plain) return `${LABELS[kind].plain} `;
if (cfg.emoji) return `${LABELS[kind].emoji} `;
return `${LABELS[kind].text} `;
}
export function formatStatsLine(
stats: { likeCount?: number | null; retweetCount?: number | null; replyCount?: number | null },
cfg: OutputConfig,
): string {
const likeCount = stats.likeCount ?? 0;
const retweetCount = stats.retweetCount ?? 0;
const replyCount = stats.replyCount ?? 0;
if (cfg.plain) return `likes: ${likeCount} retweets: ${retweetCount} replies: ${replyCount}`;
if (!cfg.emoji) return `Likes ${likeCount} Retweets ${retweetCount} Replies ${replyCount}`;
return `❤️ ${likeCount} 🔁 ${retweetCount} 💬 ${replyCount}`;
}
+12 -1
View File
@@ -2,7 +2,18 @@ import { describe, expect, it } from 'vitest';
import { looksLikeTweetInput, resolveCliInvocation } from '../src/lib/cli-args.js'; import { looksLikeTweetInput, resolveCliInvocation } from '../src/lib/cli-args.js';
describe('cli-args', () => { describe('cli-args', () => {
const known = new Set(['tweet', 'reply', 'read', 'replies', 'thread', 'search', 'mentions', 'whoami', 'check']); const known = new Set([
'tweet',
'reply',
'read',
'replies',
'thread',
'search',
'mentions',
'help',
'whoami',
'check',
]);
it('detects tweet URLs', () => { it('detects tweet URLs', () => {
expect(looksLikeTweetInput('https://x.com/user/status/1234567890')).toBe(true); expect(looksLikeTweetInput('https://x.com/user/status/1234567890')).toBe(true);
+22
View File
@@ -0,0 +1,22 @@
import { describe, expect, it } from 'vitest';
import { labelPrefix, resolveOutputConfigFromArgv, statusPrefix } from '../src/lib/output.js';
describe('output', () => {
it('plain disables emoji + color', () => {
const cfg = resolveOutputConfigFromArgv(['--plain'], {}, true);
expect(cfg).toEqual({ plain: true, emoji: false, color: false });
expect(statusPrefix('ok', cfg)).toBe('[ok] ');
expect(labelPrefix('url', cfg)).toBe('url: ');
});
it('NO_COLOR disables colors by default', () => {
const cfg = resolveOutputConfigFromArgv([], { NO_COLOR: '1' }, true);
expect(cfg.color).toBe(false);
});
it('--no-emoji switches to text prefixes', () => {
const cfg = resolveOutputConfigFromArgv(['--no-emoji'], {}, true);
expect(cfg.emoji).toBe(false);
expect(statusPrefix('warn', cfg)).toBe('Warning: ');
});
});