/** * reconcile-device-links.ts * * Walks `device_external_ids` rows where `configuration_item_id IS NULL` and * tries to link each to a configuration_item via cascading match strategies * (serial → MAC → hostname-in-company). Conflicts (multiple CIs match) are * logged and skipped — no auto-merge. * * Usage: * npx tsx scripts/reconcile-device-links.ts --dry-run * npx tsx scripts/reconcile-device-links.ts --dry-run --limit=10000 * npx tsx scripts/reconcile-device-links.ts --limit=5000 */ import { config } from 'dotenv'; config({ path: '.env.local' }); import { reconcileUnlinkedDevices } from '@/lib/services/device-link-reconciler'; function parseArgs() { const args = process.argv.slice(2); const dryRun = args.includes('--dry-run'); const limitArg = args.find((a) => a.startsWith('--limit=')); const limit = limitArg ? parseInt(limitArg.split('=')[1], 10) : 5000; return { dryRun, limit }; } async function main() { const { dryRun, limit } = parseArgs(); console.log( `[reconcile-device-links] starting${dryRun ? ' (DRY RUN — no writes)' : ''}; limit=${limit}` ); const start = Date.now(); const result = await reconcileUnlinkedDevices({ limit, dryRun }); const elapsed = ((Date.now() - start) / 1000).toFixed(1); console.log(''); console.log(`scanned: ${result.scanned}`); console.log(`linked: ${result.linked}${dryRun ? ' (would link)' : ''}`); console.log(`conflicts: ${result.conflicts} (multiple CIs matched — skipped)`); console.log(`unmatched: ${result.unmatched}`); console.log(''); console.log('by confidence:'); for (const [k, v] of Object.entries(result.byConfidence)) { if (v > 0) console.log(` ${k.padEnd(22)} ${v}`); } console.log(''); console.log(`done in ${elapsed}s`); // pg pool keeps the process alive otherwise. process.exit(0); } main().catch((err) => { console.error('[reconcile-device-links] fatal:', err); process.exit(1); });