{"version":3,"file":"pai-home-ChAOupZc.mjs","names":[],"sources":["../src/config/pai-home.ts"],"sourcesContent":["/**\n * pai-home.ts — the PAI_HOME namespace directory and generic per-user file\n * migration into it.\n *\n * Every PAI-owned per-user file (workers.yaml, config.json, whisper-rules.md,\n * advisor-mode.json, ...) lives under PAI_HOME (~/.claude/pai by default) so\n * nothing PAI writes can ever collide with a file Claude Code itself\n * introduces under ~/.claude. Decided 2026-09-19 — see docs/workers-config.md.\n */\n\nimport { existsSync, mkdirSync, readFileSync, copyFileSync, renameSync, readdirSync, statSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { join, dirname } from \"node:path\";\nimport { execFileSync } from \"node:child_process\";\n\n/**\n * Byte-identical check that works for files of any size. `cmp` compares by\n * streaming rather than loading either file into memory, unlike a\n * readFileSync + Buffer.compare — which throws (\"File size is greater than\n * 2 GiB\") on anything past Node's 2GiB single-read ceiling, a real limit hit\n * migrating a multi-gigabyte federation.db.\n */\nfunction filesByteIdentical(a: string, b: string): boolean {\n  try {\n    execFileSync(\"cmp\", [\"-s\", a, b], { stdio: \"pipe\" });\n    return true;\n  } catch (e) {\n    if (e && typeof e === \"object\" && \"status\" in e && (e as { status: number }).status === 1) return false;\n    // cmp missing or errored for an unrelated reason — fall back to an\n    // in-memory compare (fine for the small files this path normally sees).\n    return Buffer.compare(readFileSync(a), readFileSync(b)) === 0;\n  }\n}\n\n/** PAI_HOME (test isolation, power users) overrides the default namespace dir. */\nexport function paiHomeDir(): string {\n  return process.env.PAI_HOME || join(homedir(), \".claude\", \"pai\");\n}\n\n/** A path under PAI_HOME, e.g. `paiHomePath(\"config.json\")`. */\nexport function paiHomePath(...segments: string[]): string {\n  return join(paiHomeDir(), ...segments);\n}\n\nconst noticesPrinted = new Set<string>();\n\n/**\n * Resolve a per-user PAI file: the new PAI_HOME path if it exists, else the\n * first existing entry in `oldCandidates` (checked in order — most recent\n * old location first), else the new path (the target a first write\n * creates). Prints one stderr notice per process per new-path when a\n * fallback location is actually used.\n */\nexport function resolvePaiFile(newPath: string, oldCandidates: string[], migrateHint: string): string {\n  if (existsSync(newPath)) return newPath;\n  for (const old of oldCandidates) {\n    if (existsSync(old)) {\n      if (!noticesPrinted.has(newPath)) {\n        noticesPrinted.add(newPath);\n        process.stderr.write(\n          `pai: ${old} is at an old location — run \\`${migrateHint}\\` to move it to ${newPath}\\n`\n        );\n      }\n      return old;\n    }\n  }\n  return newPath;\n}\n\nexport class PaiFileMigrationError extends Error {}\n\nexport interface MigrateFileResult {\n  fromPath: string | null;\n  toPath: string;\n  dryRun: boolean;\n  note?: string;\n}\n\n/**\n * Move a per-user PAI file to its new PAI_HOME location: copy, verify\n * byte-identical, then rename the source to `<name>.migrated-<YYYYMMDD>`\n * (never deleted). Idempotent: if the new path already holds bytes\n * identical to the found source, the source is just renamed aside; if it\n * differs, this refuses rather than overwrite silently.\n */\nexport function migratePaiFile(\n  newPath: string,\n  oldCandidates: string[],\n  opts: { dryRun?: boolean } = {}\n): MigrateFileResult {\n  const fromPath = oldCandidates.find((p) => existsSync(p)) ?? null;\n  if (!fromPath) {\n    const note = existsSync(newPath) ? \"already at new location\" : \"nothing to migrate — file does not exist yet\";\n    return { fromPath: null, toPath: newPath, dryRun: !!opts.dryRun, note };\n  }\n  if (opts.dryRun) return { fromPath, toPath: newPath, dryRun: true };\n\n  const stamp = new Date().toISOString().slice(0, 10).replace(/-/g, \"\");\n\n  if (existsSync(newPath)) {\n    if (filesByteIdentical(fromPath, newPath)) {\n      renameSync(fromPath, `${fromPath}.migrated-${stamp}`);\n      return { fromPath, toPath: newPath, dryRun: false, note: \"identical — old file renamed aside\" };\n    }\n    throw new PaiFileMigrationError(\n      `${newPath} already exists and differs from ${fromPath} — resolve manually, nothing changed`\n    );\n  }\n\n  const dir = dirname(newPath);\n  if (!existsSync(dir)) mkdirSync(dir, { recursive: true });\n  copyFileSync(fromPath, newPath);\n\n  if (!filesByteIdentical(fromPath, newPath)) {\n    throw new PaiFileMigrationError(\n      `${newPath}: copy did not match ${fromPath} byte-for-byte — aborting, old file left in place`\n    );\n  }\n\n  renameSync(fromPath, `${fromPath}.migrated-${stamp}`);\n  return { fromPath, toPath: newPath, dryRun: false };\n}\n\nexport interface MigrateDirResult {\n  fromDir: string | null;\n  toDir: string;\n  dryRun: boolean;\n  movedCount?: number;\n  note?: string;\n}\n\n/**\n * Move a per-user PAI directory (queries/, session-state/, ...) into\n * PAI_HOME: move every entry from the first found old candidate into the\n * new dir (never overwriting an existing entry there), then rename the now-\n * empty old dir aside to `<name>.migrated-<YYYYMMDD>` (never deleted).\n * Idempotent — an already-migrated dir has nothing left to find.\n */\nexport function migratePaiDir(\n  newDir: string,\n  oldCandidates: string[],\n  opts: { dryRun?: boolean } = {}\n): MigrateDirResult {\n  const fromDir = oldCandidates.find((p) => existsSync(p) && statSync(p).isDirectory()) ?? null;\n  if (!fromDir) {\n    const note = existsSync(newDir) ? \"already at new location\" : \"nothing to migrate — directory does not exist yet\";\n    return { fromDir: null, toDir: newDir, dryRun: !!opts.dryRun, note };\n  }\n\n  const entries = readdirSync(fromDir);\n  if (opts.dryRun) return { fromDir, toDir: newDir, dryRun: true, movedCount: entries.length };\n\n  if (!existsSync(newDir)) mkdirSync(newDir, { recursive: true });\n\n  let moved = 0;\n  const collided: string[] = [];\n  for (const entry of entries) {\n    const src = join(fromDir, entry);\n    const dst = join(newDir, entry);\n    if (existsSync(dst)) {\n      collided.push(entry);\n      continue;\n    }\n    renameSync(src, dst);\n    moved++;\n  }\n\n  const stamp = new Date().toISOString().slice(0, 10).replace(/-/g, \"\");\n  const remaining = readdirSync(fromDir);\n  const note = collided.length\n    ? `${collided.length} entrie(s) left in ${fromDir} — name already existed in ${newDir}`\n    : undefined;\n\n  if (remaining.length === 0) {\n    renameSync(fromDir, `${fromDir}.migrated-${stamp}`);\n  }\n\n  return { fromDir, toDir: newDir, dryRun: false, movedCount: moved, note };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;AAsBA,SAAS,mBAAmB,GAAW,GAAoB;AACzD,KAAI;AACF,eAAa,OAAO;GAAC;GAAM;GAAG;GAAE,EAAE,EAAE,OAAO,QAAQ,CAAC;AACpD,SAAO;UACA,GAAG;AACV,MAAI,KAAK,OAAO,MAAM,YAAY,YAAY,KAAM,EAAyB,WAAW,EAAG,QAAO;AAGlG,SAAO,OAAO,QAAQ,aAAa,EAAE,EAAE,aAAa,EAAE,CAAC,KAAK;;;;AAKhE,SAAgB,aAAqB;AACnC,QAAO,QAAQ,IAAI,YAAY,KAAK,SAAS,EAAE,WAAW,MAAM;;;AAIlE,SAAgB,YAAY,GAAG,UAA4B;AACzD,QAAO,KAAK,YAAY,EAAE,GAAG,SAAS;;AAGxC,MAAM,iCAAiB,IAAI,KAAa;;;;;;;;AASxC,SAAgB,eAAe,SAAiB,eAAyB,aAA6B;AACpG,KAAI,WAAW,QAAQ,CAAE,QAAO;AAChC,MAAK,MAAM,OAAO,cAChB,KAAI,WAAW,IAAI,EAAE;AACnB,MAAI,CAAC,eAAe,IAAI,QAAQ,EAAE;AAChC,kBAAe,IAAI,QAAQ;AAC3B,WAAQ,OAAO,MACb,QAAQ,IAAI,iCAAiC,YAAY,mBAAmB,QAAQ,IACrF;;AAEH,SAAO;;AAGX,QAAO;;AAGT,IAAa,wBAAb,cAA2C,MAAM;;;;;;;;AAgBjD,SAAgB,eACd,SACA,eACA,OAA6B,EAAE,EACZ;CACnB,MAAM,WAAW,cAAc,MAAM,MAAM,WAAW,EAAE,CAAC,IAAI;AAC7D,KAAI,CAAC,UAAU;EACb,MAAM,OAAO,WAAW,QAAQ,GAAG,4BAA4B;AAC/D,SAAO;GAAE,UAAU;GAAM,QAAQ;GAAS,QAAQ,CAAC,CAAC,KAAK;GAAQ;GAAM;;AAEzE,KAAI,KAAK,OAAQ,QAAO;EAAE;EAAU,QAAQ;EAAS,QAAQ;EAAM;CAEnE,MAAM,yBAAQ,IAAI,MAAM,EAAC,aAAa,CAAC,MAAM,GAAG,GAAG,CAAC,QAAQ,MAAM,GAAG;AAErE,KAAI,WAAW,QAAQ,EAAE;AACvB,MAAI,mBAAmB,UAAU,QAAQ,EAAE;AACzC,cAAW,UAAU,GAAG,SAAS,YAAY,QAAQ;AACrD,UAAO;IAAE;IAAU,QAAQ;IAAS,QAAQ;IAAO,MAAM;IAAsC;;AAEjG,QAAM,IAAI,sBACR,GAAG,QAAQ,mCAAmC,SAAS,sCACxD;;CAGH,MAAM,MAAM,QAAQ,QAAQ;AAC5B,KAAI,CAAC,WAAW,IAAI,CAAE,WAAU,KAAK,EAAE,WAAW,MAAM,CAAC;AACzD,cAAa,UAAU,QAAQ;AAE/B,KAAI,CAAC,mBAAmB,UAAU,QAAQ,CACxC,OAAM,IAAI,sBACR,GAAG,QAAQ,uBAAuB,SAAS,mDAC5C;AAGH,YAAW,UAAU,GAAG,SAAS,YAAY,QAAQ;AACrD,QAAO;EAAE;EAAU,QAAQ;EAAS,QAAQ;EAAO;;;;;;;;;AAkBrD,SAAgB,cACd,QACA,eACA,OAA6B,EAAE,EACb;CAClB,MAAM,UAAU,cAAc,MAAM,MAAM,WAAW,EAAE,IAAI,SAAS,EAAE,CAAC,aAAa,CAAC,IAAI;AACzF,KAAI,CAAC,SAAS;EACZ,MAAM,OAAO,WAAW,OAAO,GAAG,4BAA4B;AAC9D,SAAO;GAAE,SAAS;GAAM,OAAO;GAAQ,QAAQ,CAAC,CAAC,KAAK;GAAQ;GAAM;;CAGtE,MAAM,UAAU,YAAY,QAAQ;AACpC,KAAI,KAAK,OAAQ,QAAO;EAAE;EAAS,OAAO;EAAQ,QAAQ;EAAM,YAAY,QAAQ;EAAQ;AAE5F,KAAI,CAAC,WAAW,OAAO,CAAE,WAAU,QAAQ,EAAE,WAAW,MAAM,CAAC;CAE/D,IAAI,QAAQ;CACZ,MAAM,WAAqB,EAAE;AAC7B,MAAK,MAAM,SAAS,SAAS;EAC3B,MAAM,MAAM,KAAK,SAAS,MAAM;EAChC,MAAM,MAAM,KAAK,QAAQ,MAAM;AAC/B,MAAI,WAAW,IAAI,EAAE;AACnB,YAAS,KAAK,MAAM;AACpB;;AAEF,aAAW,KAAK,IAAI;AACpB;;CAGF,MAAM,yBAAQ,IAAI,MAAM,EAAC,aAAa,CAAC,MAAM,GAAG,GAAG,CAAC,QAAQ,MAAM,GAAG;CACrE,MAAM,YAAY,YAAY,QAAQ;CACtC,MAAM,OAAO,SAAS,SAClB,GAAG,SAAS,OAAO,qBAAqB,QAAQ,6BAA6B,WAC7E;AAEJ,KAAI,UAAU,WAAW,EACvB,YAAW,SAAS,GAAG,QAAQ,YAAY,QAAQ;AAGrD,QAAO;EAAE;EAAS,OAAO;EAAQ,QAAQ;EAAO,YAAY;EAAO;EAAM"}