{"version":3,"file":"db-doctor.cjs","names":["pluginMigrationSlug","getMigrationStorage","extractExpectedTables"],"sources":["../../src/cli/db-doctor.ts"],"sourcesContent":["import { createHash } from \"node:crypto\";\nimport { existsSync, readFileSync } from \"node:fs\";\nimport { join, resolve } from \"node:path\";\nimport { extractExpectedTables, getMigrationStorage, pluginMigrationSlug } from \"../db\";\nimport type { PluginDbInfo } from \"./db-studio\";\n\nexport interface DoctorReport {\n  plugin: string;\n  slug: string;\n  journalTable: string;\n  journalSchema: string;\n  dbSecret: string;\n  dbUrl: string;\n  workspaceDir: string | undefined;\n  localMigrationCount: number;\n  appliedHashCount: number;\n  expectedTables: string[];\n  missingTables: string[];\n  migrationHashes: string[];\n  diagnosis: string;\n  error?: string;\n}\n\nfunction hashFile(raw: string): string {\n  return createHash(\"sha256\").update(raw).digest(\"hex\");\n}\n\ninterface LocalMigration {\n  tag: string;\n  hash: string;\n  sql: string[];\n}\n\nfunction readLocalMigrations(workspaceDir: string): LocalMigration[] {\n  const migrationsDir = resolve(workspaceDir, \"src/db/migrations\");\n  const metaDir = join(migrationsDir, \"meta\");\n  const journalPath = join(metaDir, \"_journal.json\");\n  if (!existsSync(journalPath)) return [];\n\n  const journal = JSON.parse(readFileSync(journalPath, \"utf8\")) as {\n    entries: { idx: number; tag: string }[];\n  };\n\n  return journal.entries.map((entry) => {\n    const sqlPath = join(migrationsDir, `${entry.tag}.sql`);\n    if (!existsSync(sqlPath)) {\n      return { tag: entry.tag, hash: \"\", sql: [] };\n    }\n    const raw = readFileSync(sqlPath, \"utf8\");\n    const sql = raw.split(\"--> statement-breakpoint\").map((s) => s.trim());\n    return { tag: entry.tag, hash: hashFile(raw), sql };\n  });\n}\n\nexport async function diagnosePlugin(info: PluginDbInfo): Promise<DoctorReport> {\n  const { Pool } = await import(\"pg\");\n\n  const slug = pluginMigrationSlug(info.key);\n  const storage = getMigrationStorage(slug);\n  const journalTable = storage.table;\n  const journalSchema = storage.schema;\n  const journalRef = `\"${journalSchema}\".\"${journalTable}\"`;\n\n  const localMigrations = info.workspaceDir ? readLocalMigrations(info.workspaceDir) : [];\n\n  const expectedTables = extractExpectedTables(localMigrations);\n  const localHashes = localMigrations.map((m) => m.hash).filter(Boolean);\n  const migrationHashes = localHashes;\n\n  const pool = new Pool({\n    connectionString: info.databaseUrl,\n    ssl:\n      info.databaseUrl.includes(\"localhost\") || info.databaseUrl.includes(\"127.0.0.1\")\n        ? false\n        : { rejectUnauthorized: false },\n    max: 1,\n    connectionTimeoutMillis: 10_000,\n  });\n\n  try {\n    const journalExists = await pool.query(`\n      SELECT EXISTS (\n        SELECT 1 FROM information_schema.tables\n        WHERE table_schema = '${journalSchema}' AND table_name = '${journalTable}'\n      ) AS exists\n    `);\n\n    let appliedHashCount = 0;\n    if (journalExists.rows[0]?.exists) {\n      const result = await pool.query(`SELECT hash FROM ${journalRef}`);\n      appliedHashCount = result.rows.length;\n    }\n\n    let missingTables: string[] = [];\n    if (expectedTables.length > 0) {\n      const tableResult = await pool.query(\n        `\n        SELECT table_name FROM information_schema.tables\n        WHERE table_schema = 'public'\n          AND table_name = ANY($1)\n      `,\n        [expectedTables],\n      );\n      const existing = new Set(tableResult.rows.map((r: any) => r.table_name));\n      missingTables = expectedTables.filter((t) => !existing.has(t));\n    }\n\n    let diagnosis: string;\n    if (localMigrations.length === 0) {\n      diagnosis = \"no-local-migrations\";\n    } else if (appliedHashCount === 0 && missingTables.length === 0 && localHashes.length > 0) {\n      // Journal is empty but all expected public tables exist.\n      diagnosis = \"untracked-existing-schema\";\n    } else if (appliedHashCount === 0 && localHashes.length > 0) {\n      diagnosis = \"unapplied\";\n    } else if (missingTables.length === 0) {\n      diagnosis = \"healthy\";\n    } else if (missingTables.length === expectedTables.length) {\n      diagnosis = \"drift-safe-repair\";\n    } else {\n      diagnosis = \"drift-manual\";\n    }\n\n    const masked = info.databaseUrl.replace(/\\/\\/[^:]+:[^@]+@/, \"//***:***@\");\n\n    return {\n      plugin: info.key,\n      slug,\n      journalTable,\n      journalSchema,\n      dbSecret: info.databaseSecret,\n      dbUrl: masked,\n      workspaceDir: info.workspaceDir,\n      localMigrationCount: localMigrations.length,\n      appliedHashCount,\n      expectedTables,\n      missingTables,\n      migrationHashes,\n      diagnosis,\n    };\n  } catch (error) {\n    return {\n      plugin: info.key,\n      slug,\n      journalTable,\n      journalSchema,\n      dbSecret: info.databaseSecret,\n      dbUrl: info.databaseUrl.replace(/\\/\\/[^:]+:[^@]+@/, \"//***:***@\"),\n      workspaceDir: info.workspaceDir,\n      localMigrationCount: localMigrations.length,\n      appliedHashCount: 0,\n      expectedTables,\n      missingTables: [],\n      migrationHashes: [],\n      diagnosis: \"error\",\n      error: error instanceof Error ? error.message : String(error),\n    };\n  } finally {\n    await pool.end().catch(() => {});\n  }\n}\n"],"mappings":"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"}