feat: add runtime feature overrides

This commit is contained in:
Peter Steinberger
2026-01-05 01:23:32 +01:00
parent a3eb372494
commit f5402f2b43
7 changed files with 366 additions and 19 deletions
+38 -1
View File
@@ -1,7 +1,25 @@
import type { Command } from 'commander';
import type { CliContext } from '../cli/shared.js';
import {
type FeatureOverrides,
getFeatureOverridesSnapshot,
refreshFeatureOverridesCache,
} from '../lib/runtime-features.js';
import { runtimeQueryIds } from '../lib/runtime-query-ids.js';
function countFeatureOverrides(overrides: FeatureOverrides): number {
let count = 0;
if (overrides.global) {
count += Object.keys(overrides.global).length;
}
if (overrides.sets) {
for (const setOverrides of Object.values(overrides.sets)) {
count += Object.keys(setOverrides).length;
}
}
return count;
}
export function registerQueryIdsCommand(program: Command, ctx: CliContext): void {
program
.command('query-ids')
@@ -25,16 +43,31 @@ export function registerQueryIdsCommand(program: Command, ctx: CliContext): void
if (cmdOpts.fresh) {
console.error(`${ctx.p('info')}Refreshing GraphQL query IDs…`);
await runtimeQueryIds.refresh(operations, { force: true });
console.error(`${ctx.p('info')}Refreshing feature overrides…`);
await refreshFeatureOverridesCache();
}
const featureSnapshot = getFeatureOverridesSnapshot();
const info = await runtimeQueryIds.getSnapshotInfo();
if (!info) {
if (cmdOpts.json) {
console.log(JSON.stringify({ cached: false, cachePath: runtimeQueryIds.cachePath }, null, 2));
console.log(
JSON.stringify(
{
cached: false,
cachePath: runtimeQueryIds.cachePath,
featuresPath: featureSnapshot.cachePath,
features: featureSnapshot.overrides,
},
null,
2,
),
);
return;
}
console.log(`${ctx.p('warn')}No cached query IDs yet.`);
console.log(`${ctx.p('info')}Run: bird query-ids --fresh`);
console.log(`features_path: ${featureSnapshot.cachePath}`);
return;
}
@@ -49,6 +82,8 @@ export function registerQueryIdsCommand(program: Command, ctx: CliContext): void
ageMs: info.ageMs,
ids: info.snapshot.ids,
discovery: info.snapshot.discovery,
featuresPath: featureSnapshot.cachePath,
features: featureSnapshot.overrides,
},
null,
2,
@@ -62,5 +97,7 @@ export function registerQueryIdsCommand(program: Command, ctx: CliContext): void
console.log(`fetched_at: ${info.snapshot.fetchedAt}`);
console.log(`fresh: ${info.isFresh ? 'yes' : 'no'}`);
console.log(`ops: ${Object.keys(info.snapshot.ids).length}`);
console.log(`features_path: ${featureSnapshot.cachePath}`);
console.log(`features: ${countFeatureOverrides(featureSnapshot.overrides)}`);
});
}
+5
View File
@@ -0,0 +1,5 @@
{
"global": {
"responsive_web_grok_annotations_enabled": false
}
}
+184
View File
@@ -0,0 +1,184 @@
import { existsSync, readFileSync } from 'node:fs';
import { mkdir, writeFile } from 'node:fs/promises';
import { homedir } from 'node:os';
import path from 'node:path';
// biome-ignore lint/correctness/useImportExtensions: JSON module import doesn't use .js extension.
import defaultOverrides from './features.json' with { type: 'json' };
export type FeatureOverrides = {
global?: Record<string, boolean>;
sets?: Record<string, Record<string, boolean>>;
};
type NormalizedFeatureOverrides = {
global: Record<string, boolean>;
sets: Record<string, Record<string, boolean>>;
};
export type FeatureOverridesSnapshot = {
cachePath: string;
overrides: FeatureOverrides;
};
const DEFAULT_CACHE_FILENAME = 'features.json';
let cachedOverrides: NormalizedFeatureOverrides | null = null;
function normalizeFeatureMap(value: unknown): Record<string, boolean> {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
return {};
}
const result: Record<string, boolean> = {};
for (const [key, entry] of Object.entries(value as Record<string, unknown>)) {
if (typeof entry === 'boolean') {
result[key] = entry;
}
}
return result;
}
function normalizeOverrides(value: unknown): NormalizedFeatureOverrides {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
return { global: {}, sets: {} };
}
const record = value as Record<string, unknown>;
const global = normalizeFeatureMap(record.global);
const sets: Record<string, Record<string, boolean>> = {};
const rawSets =
record.sets && typeof record.sets === 'object' && !Array.isArray(record.sets)
? (record.sets as Record<string, unknown>)
: {};
for (const [setName, setValue] of Object.entries(rawSets)) {
const normalized = normalizeFeatureMap(setValue);
if (Object.keys(normalized).length > 0) {
sets[setName] = normalized;
}
}
return { global, sets };
}
function mergeOverrides(
base: NormalizedFeatureOverrides,
next: NormalizedFeatureOverrides,
): NormalizedFeatureOverrides {
const sets: Record<string, Record<string, boolean>> = { ...base.sets };
for (const [setName, overrides] of Object.entries(next.sets)) {
const existing = sets[setName];
sets[setName] = existing ? { ...existing, ...overrides } : { ...overrides };
}
return {
global: { ...base.global, ...next.global },
sets,
};
}
function toFeatureOverrides(overrides: NormalizedFeatureOverrides): FeatureOverrides {
const result: FeatureOverrides = {};
if (Object.keys(overrides.global).length > 0) {
result.global = overrides.global;
}
const setEntries = Object.entries(overrides.sets).filter(([, value]) => Object.keys(value).length > 0);
if (setEntries.length > 0) {
result.sets = Object.fromEntries(setEntries);
}
return result;
}
function resolveFeaturesCachePath(): string {
const override = process.env.BIRD_FEATURES_CACHE ?? process.env.BIRD_FEATURES_PATH;
if (override && override.trim().length > 0) {
return path.resolve(override.trim());
}
return path.join(homedir(), '.config', 'bird', DEFAULT_CACHE_FILENAME);
}
function readOverridesFromFile(cachePath: string): NormalizedFeatureOverrides | null {
if (!existsSync(cachePath)) {
return null;
}
try {
const raw = readFileSync(cachePath, 'utf8');
return normalizeOverrides(JSON.parse(raw));
} catch {
return null;
}
}
function readOverridesFromEnv(): NormalizedFeatureOverrides | null {
const raw = process.env.BIRD_FEATURES_JSON;
if (!raw || raw.trim().length === 0) {
return null;
}
try {
return normalizeOverrides(JSON.parse(raw));
} catch {
return null;
}
}
function writeOverridesToDisk(cachePath: string, overrides: NormalizedFeatureOverrides): Promise<void> {
const payload = toFeatureOverrides(overrides);
return mkdir(path.dirname(cachePath), { recursive: true }).then(() =>
writeFile(cachePath, `${JSON.stringify(payload, null, 2)}\n`, 'utf8'),
);
}
export function loadFeatureOverrides(): NormalizedFeatureOverrides {
if (cachedOverrides) {
return cachedOverrides;
}
const base = normalizeOverrides(defaultOverrides);
const fromFile = readOverridesFromFile(resolveFeaturesCachePath());
const fromEnv = readOverridesFromEnv();
let merged = base;
if (fromFile) {
merged = mergeOverrides(merged, fromFile);
}
if (fromEnv) {
merged = mergeOverrides(merged, fromEnv);
}
cachedOverrides = merged;
return merged;
}
export function getFeatureOverridesSnapshot(): FeatureOverridesSnapshot {
const overrides = toFeatureOverrides(loadFeatureOverrides());
return {
cachePath: resolveFeaturesCachePath(),
overrides,
};
}
export function applyFeatureOverrides(setName: string, base: Record<string, boolean>): Record<string, boolean> {
const overrides = loadFeatureOverrides();
const globalOverrides = overrides.global;
const setOverrides = overrides.sets[setName];
if (Object.keys(globalOverrides).length === 0 && (!setOverrides || Object.keys(setOverrides).length === 0)) {
return base;
}
if (setOverrides) {
return {
...base,
...globalOverrides,
...setOverrides,
};
}
return {
...base,
...globalOverrides,
};
}
export async function refreshFeatureOverridesCache(): Promise<FeatureOverridesSnapshot> {
const cachePath = resolveFeaturesCachePath();
const base = normalizeOverrides(defaultOverrides);
const fromFile = readOverridesFromFile(cachePath);
const merged = mergeOverrides(base, fromFile ?? { global: {}, sets: {} });
await writeOverridesToDisk(cachePath, merged);
cachedOverrides = null;
return { cachePath, overrides: toFeatureOverrides(merged) };
}
export function clearFeatureOverridesCache(): void {
cachedOverrides = null;
}
+19 -17
View File
@@ -1,5 +1,7 @@
import { applyFeatureOverrides } from './runtime-features.js';
export function buildArticleFeatures(): Record<string, boolean> {
return {
return applyFeatureOverrides('article', {
rweb_video_screen_enabled: true,
profile_label_improvements_pcf_label_in_post_enabled: true,
responsive_web_profile_redirect_enabled: true,
@@ -35,11 +37,11 @@ export function buildArticleFeatures(): Record<string, boolean> {
responsive_web_grok_imagine_annotation_enabled: true,
responsive_web_grok_community_note_auto_translation_is_enabled: false,
responsive_web_enhance_cards_enabled: false,
};
});
}
export function buildTweetDetailFeatures(): Record<string, boolean> {
return {
return applyFeatureOverrides('tweetDetail', {
...buildArticleFeatures(),
responsive_web_graphql_exclude_directive_enabled: true,
communities_web_enable_tweet_community_results_fetch: true,
@@ -52,7 +54,7 @@ export function buildTweetDetailFeatures(): Record<string, boolean> {
tweet_awards_web_tipping_enabled: false,
creator_subscriptions_quote_tweet_preview_enabled: false,
verified_phone_label_enabled: false,
};
});
}
export function buildArticleFieldToggles(): Record<string, boolean> {
@@ -67,7 +69,7 @@ export function buildArticleFieldToggles(): Record<string, boolean> {
}
export function buildSearchFeatures(): Record<string, boolean> {
return {
return applyFeatureOverrides('search', {
rweb_video_screen_enabled: true,
profile_label_improvements_pcf_label_in_post_enabled: true,
responsive_web_profile_redirect_enabled: true,
@@ -105,11 +107,11 @@ export function buildSearchFeatures(): Record<string, boolean> {
responsive_web_grok_community_note_auto_translation_is_enabled: false,
articles_preview_enabled: true,
responsive_web_enhance_cards_enabled: false,
};
});
}
export function buildTweetCreateFeatures(): Record<string, boolean> {
return {
return applyFeatureOverrides('tweetCreate', {
rweb_video_screen_enabled: true,
creator_subscriptions_tweet_preview_api_enabled: true,
premium_content_api_read_enabled: false,
@@ -145,11 +147,11 @@ export function buildTweetCreateFeatures(): Record<string, boolean> {
responsive_web_grok_imagine_annotation_enabled: true,
responsive_web_graphql_timeline_navigation_enabled: true,
responsive_web_enhance_cards_enabled: false,
};
});
}
export function buildTimelineFeatures(): Record<string, boolean> {
return {
return applyFeatureOverrides('timeline', {
...buildSearchFeatures(),
blue_business_profile_image_shape_enabled: true,
responsive_web_text_conversations_enabled: false,
@@ -159,22 +161,22 @@ export function buildTimelineFeatures(): Record<string, boolean> {
interactive_text_enabled: true,
longform_notetweets_richtext_consumption_enabled: true,
responsive_web_media_download_video_enabled: false,
};
});
}
export function buildBookmarksFeatures(): Record<string, boolean> {
return {
return applyFeatureOverrides('bookmarks', {
...buildTimelineFeatures(),
graphql_timeline_v2_bookmark_timeline: true,
};
});
}
export function buildLikesFeatures(): Record<string, boolean> {
return buildTimelineFeatures();
return applyFeatureOverrides('likes', buildTimelineFeatures());
}
export function buildListsFeatures(): Record<string, boolean> {
return {
return applyFeatureOverrides('lists', {
rweb_video_screen_enabled: true,
profile_label_improvements_pcf_label_in_post_enabled: true,
responsive_web_profile_redirect_enabled: true,
@@ -210,11 +212,11 @@ export function buildListsFeatures(): Record<string, boolean> {
responsive_web_grok_imagine_annotation_enabled: true,
responsive_web_grok_community_note_auto_translation_is_enabled: false,
responsive_web_enhance_cards_enabled: false,
};
});
}
export function buildFollowingFeatures(): Record<string, boolean> {
return {
return applyFeatureOverrides('following', {
rweb_video_screen_enabled: true,
profile_label_improvements_pcf_label_in_post_enabled: false,
responsive_web_profile_redirect_enabled: true,
@@ -250,5 +252,5 @@ export function buildFollowingFeatures(): Record<string, boolean> {
responsive_web_grok_imagine_annotation_enabled: false,
responsive_web_grok_community_note_auto_translation_is_enabled: false,
responsive_web_enhance_cards_enabled: false,
};
});
}