import { existsSync, readFileSync } from "node:fs"; import process from "node:process"; import { createBacklinksService } from "@/server/features/backlinks/services/BacklinksService"; import type { BillingCustomerContext } from "@/server/billing/subscription"; import type { BacklinksLookupInput, BacklinksTargetScope, } from "@/types/schemas/backlinks"; loadLocalEnv(); const args = parseArgs(process.argv.slice(2)); const inMemoryCache = new Map(); const service = createBacklinksService({ async get(key) { const raw = inMemoryCache.get(key); return raw ? parseCachedValue(raw) : null; }, async set(key, data) { inMemoryCache.set(key, JSON.stringify(data)); }, }); await main(); async function main() { if (process.env.CI === "true" && args.allowCi !== "true") { printUsageAndExit( "Refusing to run live billing checks in CI without --allowCi=true.", ); } if (args.confirmLive !== "true") { printUsageAndExit( "This command makes live, billable DataForSEO requests. Re-run with --confirmLive=true.", ); } const input = buildInput(args); const billingCustomer = buildBillingCustomer(args); const repeat = parsePositiveInteger(args.repeat, 1); const includeTabs = parseBoolean(args.includeTabs, true); const runs = []; for (let index = 0; index < repeat; index += 1) { const overview = await service.profileOverview(input, billingCustomer); const domains = includeTabs ? await service.profileReferringDomains(input, billingCustomer) : null; const pages = includeTabs ? await service.profileTopPages(input, billingCustomer) : null; runs.push({ run: index + 1, overview: { backlinksRows: overview.overview.backlinks.length, trendRows: overview.overview.trends.length, newLostRows: overview.overview.newLostTrends.length, }, domainsTab: domains ? { rows: domains.rows.length, } : null, pagesTab: pages ? { rows: pages.rows.length, } : null, }); } console.log( JSON.stringify( { input, repeat, includeTabs, runs, }, null, 2, ), ); } function buildInput(cliArgs: Record): BacklinksLookupInput { const target = cliArgs.target; const scope = parseScope(cliArgs.scope); if (!target) { printUsageAndExit("Missing target."); } if (!process.env.DATAFORSEO_API_KEY) { printUsageAndExit("Missing DATAFORSEO_API_KEY."); } return { target, scope, }; } function buildBillingCustomer( cliArgs: Record, ): BillingCustomerContext { return { organizationId: cliArgs.organizationId ?? "local", userId: cliArgs.userId ?? "local-user", userEmail: cliArgs.userEmail ?? "local@example.com", }; } function parseArgs(argv: string[]) { const parsed: Record = {}; for (let index = 0; index < argv.length; index += 1) { const token = argv[index]; if (!token.startsWith("--")) continue; const withoutPrefix = token.slice(2); const separatorIndex = withoutPrefix.indexOf("="); if (separatorIndex >= 0) { parsed[withoutPrefix.slice(0, separatorIndex)] = withoutPrefix.slice( separatorIndex + 1, ); continue; } const next = argv[index + 1]; if (!next || next.startsWith("--")) { parsed[withoutPrefix] = "true"; continue; } parsed[withoutPrefix] = next; index += 1; } return parsed; } function parseBoolean(value: string | undefined, fallback: boolean) { if (value == null) return fallback; return value === "true"; } function parseScope( value: string | undefined, ): BacklinksTargetScope | undefined { if (!value) return undefined; if (value === "domain" || value === "page") return value; printUsageAndExit(`Invalid scope: ${value}. Expected domain or page.`); } function parsePositiveInteger(value: string | undefined, fallback: number) { if (!value) return fallback; const parsed = Number.parseInt(value, 10); return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback; } function loadLocalEnv() { for (const path of [".env.local", ".env"]) { if (!existsSync(path)) continue; const content = readFileSync(path, "utf8"); for (const line of content.split(/\r?\n/u)) { const trimmed = line.trim(); if (!trimmed || trimmed.startsWith("#")) continue; const separatorIndex = trimmed.indexOf("="); if (separatorIndex < 0) continue; const key = trimmed.slice(0, separatorIndex).trim(); const rawValue = trimmed.slice(separatorIndex + 1).trim(); if (!key || process.env[key]) continue; process.env[key] = rawValue.replace(/^['"]|['"]$/g, ""); } } } function printUsageAndExit(message: string): never { console.error(message); console.error( "Usage: pnpm billing:backlinks --target=example.com --confirmLive=true [--scope=domain|page] [--repeat=1] [--includeTabs=true|false] [--allowCi=true]", ); process.exit(1); } function parseCachedValue(raw: string): unknown { try { return JSON.parse(raw) as unknown; } catch { return null; } }