{"version":3,"file":"db-doctor.cjs","names":["pluginMigrationSlug","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, 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  legacyCount: number;\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 journalTable = `__drizzle_migrations_${slug}`;\n  const journalSchema = \"drizzle\";\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    const legacyCount = await countLegacyHashes(pool, localHashes);\n\n    let diagnosis: string;\n    if (localMigrations.length === 0) {\n      diagnosis = \"no-local-migrations\";\n    } else if (appliedHashCount === 0 && localHashes.length > 0) {\n      diagnosis = legacyCount > 0 ? \"legacy-importable\" : \"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      legacyCount,\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      legacyCount: 0,\n      error: error instanceof Error ? error.message : String(error),\n    };\n  } finally {\n    await pool.end().catch(() => {});\n  }\n}\n\nasync function countLegacyHashes(pool: any, localHashes: string[]): Promise<number> {\n  if (localHashes.length === 0) return 0;\n  const candidates = [\n    { schema: \"drizzle\", table: \"__drizzle_migrations\" },\n    { schema: \"public\", table: \"drizzle_migrations\" },\n  ];\n  let total = 0;\n  for (const c of candidates) {\n    try {\n      const result = await pool.query(\n        `SELECT count(*)::int AS cnt FROM \"${c.schema}\".\"${c.table}\" WHERE hash = ANY($1)`,\n        [localHashes],\n      );\n      total += result.rows[0]?.cnt ?? 0;\n    } catch {\n      // table might not exist\n    }\n  }\n  return total;\n}\n"],"mappings":"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"}