{"version":3,"file":"legacy-databases.mjs","names":["loadDotenv"],"sources":["../../src/env/legacy-databases.ts"],"sourcesContent":["import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { config as loadDotenv } from \"dotenv\";\nimport { Data, Effect } from \"effect\";\nimport { PGLITE_DEV_DATABASE_URL, PGLITE_TEST_DATABASE_URL } from \"../services/infra\";\n\nexport class LegacyMigrationError extends Data.TaggedError(\"LegacyMigrationError\")<{\n  cause: unknown;\n}> {}\n\nexport interface LegacyMigrationReport {\n  databaseUrlDrift: Array<{ key: string; from: string; to: string }>;\n  composeRelocated: boolean;\n}\n\n/**\n * The compose-era local database convention (`bos dev` provisioned Postgres\n * via the committed docker-compose.yml): every `*_DATABASE_URL` pointed at\n * `postgres://everythingdev:everythingdev@localhost:<port>/<db>` on the\n * hardcoded 5432–5435 block. Those are exactly the opt-ins that made two\n * concurrent local projects fight over host ports.\n *\n * The compose-era convention is retired — local persistence is a\n * file-backed PGlite data dir (`.bos/db`), tests run on in-memory pglite.\n * `bos dev` repairs leftover state once, idempotently:\n *   1. `.env` lines whose URL matches the conventional shape (user, host,\n *      and database name — port too, but drift ports like 5532 from the old\n *      +100 allocator also match by name) are rewritten to the new defaults.\n *      Any other `postgres://` URL is operator-owned and left alone.\n *   2. A leftover scaffold-shaped `docker-compose.yml` is moved to\n *      `.bos/compose-retired/` rather than deleted; operator-authored\n *      compose files (different service names/images) are untouched.\n */\nexport const migrateLegacyComposeState = (\n  configDir: string,\n): Effect.Effect<LegacyMigrationReport, never> =>\n  Effect.try({\n    try: () => {\n      const envPath = join(configDir, \".env\");\n      const databaseUrlDrift: LegacyMigrationReport[\"databaseUrlDrift\"] = [];\n\n      if (existsSync(envPath)) {\n        const raw = readFileSync(envPath, \"utf-8\");\n        const parsed = loadDotenv({ path: envPath, quiet: true }).parsed ?? {};\n        const rewritten = rewrittenLegacyDatabaseValues({ file: parsed, raw });\n        if (rewritten.size > 0) {\n          const lines = raw.split(\"\\n\");\n          const updated = lines.map((line) => {\n            const match = /^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/.exec(line);\n            if (!match) return line;\n            const replacement = rewritten.get(match[1]);\n            return replacement === undefined ? line : `${match[1]}=${replacement}`;\n          });\n          writeFileSync(envPath, updated.join(\"\\n\"));\n          for (const [key, to] of rewritten) {\n            databaseUrlDrift.push({ key, from: parsed[key] ?? \"\", to });\n          }\n        }\n      }\n\n      const composeRelocated = relocateLegacyComposeFile(configDir);\n      return { databaseUrlDrift, composeRelocated };\n    },\n    catch: (cause) => new LegacyMigrationError({ cause }),\n  }).pipe(Effect.orElseSucceed(() => ({ databaseUrlDrift: [], composeRelocated: false })));\n\n/** Matches the compose-era URL shapes: the scaffolded user + localhost + the convention's database names. */\nconst LEGACY_DATABASE_NAME = /^(api_db|auth_db|api_test_db|auth_test_db)$/;\n\nfunction legacyMappingForUrl(rawUrl: string): string | undefined {\n  try {\n    const url = new URL(rawUrl);\n    if (url.protocol !== \"postgres:\" && url.protocol !== \"postgresql:\") return undefined;\n    if (url.hostname !== \"localhost\" && url.hostname !== \"127.0.0.1\") return undefined;\n    if (url.username !== \"everythingdev\" || url.password !== \"everythingdev\") return undefined;\n    const databaseName = decodeURIComponent(url.pathname.replace(/^\\//, \"\"));\n    if (!LEGACY_DATABASE_NAME.test(databaseName)) return undefined;\n    return databaseName.endsWith(\"_test_db\") ? PGLITE_TEST_DATABASE_URL : PGLITE_DEV_DATABASE_URL;\n  } catch {\n    return undefined;\n  }\n}\n\nexport function rewrittenLegacyDatabaseValues(input: {\n  file: Record<string, string>;\n  raw: string;\n}): Map<string, string> {\n  const rewritten = new Map<string, string>();\n  for (const [key, value] of Object.entries(input.file)) {\n    const match = value ? legacyMappingForUrl(value) : undefined;\n    if (match !== undefined) {\n      const rawLine = new RegExp(`^${key}=.+$`, \"m\").test(input.raw);\n      if (rawLine) rewritten.set(key, match);\n    }\n  }\n  return rewritten;\n}\n\nconst SCAFFOLD_SERVICE_NAMES = new Set([\n  \"postgres-api\",\n  \"postgres-auth\",\n  \"postgres-api-test\",\n  \"postgres-auth-test\",\n]);\nconst SCAFFOLD_IMAGE_HINTS = [\n  \"public.ecr.aws/docker/library/postgres\",\n  \"docker.io/library/postgres\",\n  \"postgres:17-alpine\",\n  \"ghcr.io/nearbuilders/mirror-postgres\",\n];\n\nfunction isScaffoldComposeFile(raw: string): boolean {\n  const services = [...raw.matchAll(/^\\s+-?\\s*([a-z-]+):\\s*$/gm)].map((match) => match[1] ?? \"\");\n  if (!services.some((name) => SCAFFOLD_SERVICE_NAMES.has(name))) return false;\n  return SCAFFOLD_IMAGE_HINTS.some((hint) => raw.includes(hint));\n}\n\nfunction relocateLegacyComposeFile(configDir: string): boolean {\n  const composePath = join(configDir, \"docker-compose.yml\");\n  if (!existsSync(composePath)) return false;\n  let raw: string;\n  try {\n    raw = readFileSync(composePath, \"utf-8\");\n  } catch {\n    return false;\n  }\n  if (!isScaffoldComposeFile(raw)) return false;\n  const retiredDir = join(configDir, \".bos\", \"compose-retired\");\n  mkdirSync(retiredDir, { recursive: true });\n  renameSync(composePath, join(retiredDir, \"docker-compose.yml\"));\n  return true;\n}\n"],"mappings":";;;;;;;AAMA,IAAa,uBAAb,cAA0C,KAAK,YAAY,sBAAsB,CAAC,CAE/E,CAAC;;;;;;;;;;;;;;;;;;;AAyBJ,MAAa,6BACX,cAEA,OAAO,IAAI;CACT,WAAW;EACT,MAAM,UAAU,KAAK,WAAW,MAAM;EACtC,MAAM,mBAA8D,CAAC;EAErE,IAAI,WAAW,OAAO,GAAG;GACvB,MAAM,MAAM,aAAa,SAAS,OAAO;GACzC,MAAM,SAASA,OAAW;IAAE,MAAM;IAAS,OAAO;GAAK,CAAC,CAAC,CAAC,UAAU,CAAC;GACrE,MAAM,YAAY,8BAA8B;IAAE,MAAM;IAAQ;GAAI,CAAC;GACrE,IAAI,UAAU,OAAO,GAAG;IAEtB,MAAM,UADQ,IAAI,MAAM,IACJ,CAAC,CAAC,KAAK,SAAS;KAClC,MAAM,QAAQ,kCAAkC,KAAK,IAAI;KACzD,IAAI,CAAC,OAAO,OAAO;KACnB,MAAM,cAAc,UAAU,IAAI,MAAM,EAAE;KAC1C,OAAO,gBAAgB,SAAY,OAAO,GAAG,MAAM,GAAG,GAAG;IAC3D,CAAC;IACD,cAAc,SAAS,QAAQ,KAAK,IAAI,CAAC;IACzC,KAAK,MAAM,CAAC,KAAK,OAAO,WACtB,iBAAiB,KAAK;KAAE;KAAK,MAAM,OAAO,QAAQ;KAAI;IAAG,CAAC;GAE9D;EACF;EAGA,OAAO;GAAE;GAAkB,kBADF,0BAA0B,SACT;EAAE;CAC9C;CACA,QAAQ,UAAU,IAAI,qBAAqB,EAAE,MAAM,CAAC;AACtD,CAAC,CAAC,CAAC,KAAK,OAAO,qBAAqB;CAAE,kBAAkB,CAAC;CAAG,kBAAkB;AAAM,EAAE,CAAC;;AAGzF,MAAM,uBAAuB;AAE7B,SAAS,oBAAoB,QAAoC;CAC/D,IAAI;EACF,MAAM,MAAM,IAAI,IAAI,MAAM;EAC1B,IAAI,IAAI,aAAa,eAAe,IAAI,aAAa,eAAe,OAAO;EAC3E,IAAI,IAAI,aAAa,eAAe,IAAI,aAAa,aAAa,OAAO;EACzE,IAAI,IAAI,aAAa,mBAAmB,IAAI,aAAa,iBAAiB,OAAO;EACjF,MAAM,eAAe,mBAAmB,IAAI,SAAS,QAAQ,OAAO,EAAE,CAAC;EACvE,IAAI,CAAC,qBAAqB,KAAK,YAAY,GAAG,OAAO;EACrD,OAAO,aAAa,SAAS,UAAU,IAAI,2BAA2B;CACxE,QAAQ;EACN;CACF;AACF;AAEA,SAAgB,8BAA8B,OAGtB;CACtB,MAAM,4BAAY,IAAI,IAAoB;CAC1C,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,MAAM,IAAI,GAAG;EACrD,MAAM,QAAQ,QAAQ,oBAAoB,KAAK,IAAI;EACnD,IAAI,UAAU,QAEZ;OADgB,IAAI,OAAO,IAAI,IAAI,OAAO,GAAG,CAAC,CAAC,KAAK,MAAM,GAChD,GAAG,UAAU,IAAI,KAAK,KAAK;EAAC;CAE1C;CACA,OAAO;AACT;AAEA,MAAM,yCAAyB,IAAI,IAAI;CACrC;CACA;CACA;CACA;AACF,CAAC;AACD,MAAM,uBAAuB;CAC3B;CACA;CACA;CACA;AACF;AAEA,SAAS,sBAAsB,KAAsB;CAEnD,IAAI,CADa,CAAC,GAAG,IAAI,SAAS,2BAA2B,CAAC,CAAC,CAAC,KAAK,UAAU,MAAM,MAAM,EAC/E,CAAC,CAAC,MAAM,SAAS,uBAAuB,IAAI,IAAI,CAAC,GAAG,OAAO;CACvE,OAAO,qBAAqB,MAAM,SAAS,IAAI,SAAS,IAAI,CAAC;AAC/D;AAEA,SAAS,0BAA0B,WAA4B;CAC7D,MAAM,cAAc,KAAK,WAAW,oBAAoB;CACxD,IAAI,CAAC,WAAW,WAAW,GAAG,OAAO;CACrC,IAAI;CACJ,IAAI;EACF,MAAM,aAAa,aAAa,OAAO;CACzC,QAAQ;EACN,OAAO;CACT;CACA,IAAI,CAAC,sBAAsB,GAAG,GAAG,OAAO;CACxC,MAAM,aAAa,KAAK,WAAW,QAAQ,iBAAiB;CAC5D,UAAU,YAAY,EAAE,WAAW,KAAK,CAAC;CACzC,WAAW,aAAa,KAAK,YAAY,oBAAoB,CAAC;CAC9D,OAAO;AACT"}