glassbox 1.1.0-beta.1 → 1.1.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -636,14 +636,14 @@ function prefixIssues(path, issues) {
636
636
  return iss;
637
637
  });
638
638
  }
639
- function unwrapMessage(message) {
640
- return typeof message === "string" ? message : message?.message;
639
+ function unwrapMessage(message2) {
640
+ return typeof message2 === "string" ? message2 : message2?.message;
641
641
  }
642
642
  function finalizeIssue(iss, ctx, config2) {
643
- const message = iss.message ? iss.message : unwrapMessage(iss.inst?._zod.def?.error?.(iss)) ?? unwrapMessage(ctx?.error?.(iss)) ?? unwrapMessage(config2.customError?.(iss)) ?? unwrapMessage(config2.localeError?.(iss)) ?? "Invalid input";
643
+ const message2 = iss.message ? iss.message : unwrapMessage(iss.inst?._zod.def?.error?.(iss)) ?? unwrapMessage(ctx?.error?.(iss)) ?? unwrapMessage(config2.customError?.(iss)) ?? unwrapMessage(config2.localeError?.(iss)) ?? "Invalid input";
644
644
  const { inst: _inst, continue: _continue, input: _input, ...rest } = iss;
645
645
  rest.path ?? (rest.path = []);
646
- rest.message = message;
646
+ rest.message = message2;
647
647
  if (ctx?.reportInput) {
648
648
  rest.input = _input;
649
649
  }
@@ -15681,8 +15681,8 @@ function hashArtifacts(repoRoot, artifacts) {
15681
15681
  if (/(^|\/)\.\.(\/|$)/.test(safe)) continue;
15682
15682
  try {
15683
15683
  const abs = join2(repoRoot, safe);
15684
- const stat = statSync(abs);
15685
- if (!stat.isFile() || stat.size > ARTIFACT_HASH_MAX_BYTES) continue;
15684
+ const stat4 = statSync(abs);
15685
+ if (!stat4.isFile() || stat4.size > ARTIFACT_HASH_MAX_BYTES) continue;
15686
15686
  out[uri] = createHash("sha256").update(readFileSync2(abs)).digest("hex");
15687
15687
  } catch {
15688
15688
  }
@@ -15815,8 +15815,8 @@ function readArtifactText(repoRoot, uri) {
15815
15815
  if (/(^|\/)\.\.(\/|$)/.test(safe)) return void 0;
15816
15816
  try {
15817
15817
  const abs = join2(repoRoot, safe);
15818
- const stat = statSync(abs);
15819
- if (!stat.isFile() || stat.size > ARTIFACT_MAX_BYTES) return void 0;
15818
+ const stat4 = statSync(abs);
15819
+ if (!stat4.isFile() || stat4.size > ARTIFACT_MAX_BYTES) return void 0;
15820
15820
  const buf = readFileSync2(abs);
15821
15821
  if (buf.includes(0)) return void 0;
15822
15822
  if (isLfsPointer(buf)) return void 0;
@@ -16780,38 +16780,2012 @@ var init_ddl = __esm({
16780
16780
  }
16781
16781
  });
16782
16782
 
16783
- // src/db/connection.ts
16783
+ // node_modules/pglite-migrate/dist/index.js
16784
+ import { stat } from "fs/promises";
16785
+ import { mkdir, writeFile } from "fs/promises";
16786
+ import { dirname as dirname22, join as join3, resolve as resolve4, sep } from "path";
16787
+ import { promisify } from "util";
16788
+ import { gunzip as gunzipCb } from "zlib";
16789
+ import { createHash as createHash2, randomUUID } from "crypto";
16790
+ import { mkdir as mkdir2, readFile as readFile3, rename as rename2, rm as rm2 } from "fs/promises";
16791
+ import { homedir, tmpdir } from "os";
16792
+ import { join as join4, resolve as resolve22, sep as sep2 } from "path";
16793
+ import { cp, readdir, readFile, rename, rm, stat as stat2 } from "fs/promises";
16794
+ import { basename as basename2, dirname as dirname5, join as join5 } from "path";
16795
+ import { isAbsolute as isAbsolute2 } from "path";
16796
+ import { pathToFileURL } from "url";
16797
+ import { readFile as readFile2 } from "fs/promises";
16798
+ import { join as join22 } from "path";
16799
+ import { rename as rename3, rm as rm3 } from "fs/promises";
16800
+ async function exists(path) {
16801
+ try {
16802
+ await stat(path);
16803
+ return true;
16804
+ } catch {
16805
+ return false;
16806
+ }
16807
+ }
16808
+ function sanitizedTimestamp(override) {
16809
+ return (override ?? (/* @__PURE__ */ new Date()).toISOString()).replace(/:/g, "-");
16810
+ }
16811
+ function errorCode(err) {
16812
+ if (typeof err !== "object" || err === null) return void 0;
16813
+ const code = err.code;
16814
+ return typeof code === "string" ? code : void 0;
16815
+ }
16816
+ function knownMajors() {
16817
+ return RELEASES.map((r) => r.postgresMajor);
16818
+ }
16819
+ function resolveEngine(major) {
16820
+ const release = tryResolveEngine(major);
16821
+ if (release === void 0) throw new UnknownMajorError(major, knownMajors());
16822
+ return release;
16823
+ }
16824
+ function tryResolveEngine(major) {
16825
+ return RELEASES.find((r) => r.postgresMajor === major);
16826
+ }
16827
+ function readOctal(header, offset, length) {
16828
+ if ((header[offset] & 128) !== 0) {
16829
+ throw new TarError("Unsupported base-256 numeric field in tar header");
16830
+ }
16831
+ const raw2 = header.subarray(offset, offset + length).toString("latin1").replace(/\0/g, " ").trim();
16832
+ if (raw2 === "") return 0;
16833
+ if (!/^[0-7]+$/.test(raw2)) throw new TarError(`Malformed octal field: ${JSON.stringify(raw2)}`);
16834
+ return Number.parseInt(raw2, 8);
16835
+ }
16836
+ function readString(header, offset, length) {
16837
+ const raw2 = header.subarray(offset, offset + length);
16838
+ const end = raw2.indexOf(0);
16839
+ return raw2.subarray(0, end === -1 ? raw2.length : end).toString("utf8");
16840
+ }
16841
+ function isZeroBlock(block) {
16842
+ for (const byte of block) if (byte !== 0) return false;
16843
+ return true;
16844
+ }
16845
+ function verifyChecksum(header) {
16846
+ const expected = readOctal(header, OFF.checksum, 8);
16847
+ let signed = 0;
16848
+ let unsigned = 0;
16849
+ for (let i = 0; i < BLOCK; i++) {
16850
+ const inChecksumField = i >= OFF.checksum && i < OFF.checksum + 8;
16851
+ const byte = inChecksumField ? 32 : header[i];
16852
+ unsigned += byte;
16853
+ signed += byte > 127 ? byte - 256 : byte;
16854
+ }
16855
+ if (unsigned !== expected && signed !== expected) {
16856
+ throw new TarError(
16857
+ `Tar header checksum mismatch (expected ${expected.toString()}, got ${unsigned.toString()})`
16858
+ );
16859
+ }
16860
+ }
16861
+ function safeEntryPath(name, stripComponents) {
16862
+ if (name.includes("\0")) throw new TarError(`Tar entry name contains a NUL byte`);
16863
+ if (name.includes("\\")) throw new TarError(`Tar entry name contains a backslash: ${name}`);
16864
+ if (name.startsWith("/") || /^[A-Za-z]:/.test(name)) {
16865
+ throw new TarError(`Absolute path in tar entry: ${name}`);
16866
+ }
16867
+ const parts = name.split("/").filter((p) => p !== "" && p !== ".");
16868
+ if (parts.includes("..")) throw new TarError(`Path traversal in tar entry: ${name}`);
16869
+ const stripped = parts.slice(stripComponents);
16870
+ return stripped.length === 0 ? null : stripped.join("/");
16871
+ }
16872
+ function paxPath(data) {
16873
+ const text = data.toString("utf8");
16874
+ const match = /(?:^|\n)\d+ path=([^\n]*)\n/.exec(text);
16875
+ return match?.[1];
16876
+ }
16877
+ async function extractTarGz(gzipped, dest, stripComponents = 0) {
16878
+ const buf = await gunzip(gzipped);
16879
+ const root = resolve4(dest);
16880
+ await mkdir(root, { recursive: true });
16881
+ let offset = 0;
16882
+ let filesWritten = 0;
16883
+ let nextNameOverride;
16884
+ while (offset + BLOCK <= buf.length) {
16885
+ const header = buf.subarray(offset, offset + BLOCK);
16886
+ if (isZeroBlock(header)) break;
16887
+ verifyChecksum(header);
16888
+ const size = readOctal(header, OFF.size, 12);
16889
+ const typeflag = String.fromCharCode(header[OFF.typeflag]);
16890
+ const prefix = readString(header, OFF.prefix, 155);
16891
+ const base = readString(header, OFF.name, 100);
16892
+ const rawName = nextNameOverride ?? (prefix === "" ? base : `${prefix}/${base}`);
16893
+ nextNameOverride = void 0;
16894
+ const dataStart = offset + BLOCK;
16895
+ const dataEnd = dataStart + size;
16896
+ if (dataEnd > buf.length) throw new TarError("Tar entry extends past end of archive");
16897
+ const data = buf.subarray(dataStart, dataEnd);
16898
+ offset = dataStart + Math.ceil(size / BLOCK) * BLOCK;
16899
+ switch (typeflag) {
16900
+ case TYPE.paxNext: {
16901
+ nextNameOverride = paxPath(data);
16902
+ continue;
16903
+ }
16904
+ case TYPE.gnuLongName: {
16905
+ nextNameOverride = data.toString("utf8").replace(/\0+$/, "");
16906
+ continue;
16907
+ }
16908
+ case TYPE.paxGlobal:
16909
+ continue;
16910
+ // global metadata, nothing to write
16911
+ case TYPE.file:
16912
+ case TYPE.fileAlt:
16913
+ case TYPE.contiguous: {
16914
+ const rel = safeEntryPath(rawName, stripComponents);
16915
+ if (rel === null) continue;
16916
+ const full = resolve4(root, rel);
16917
+ if (full !== root && !full.startsWith(root + sep)) {
16918
+ throw new TarError(`Tar entry escapes extraction root: ${rawName}`);
16919
+ }
16920
+ await mkdir(dirname22(full), { recursive: true });
16921
+ await writeFile(full, data);
16922
+ filesWritten++;
16923
+ continue;
16924
+ }
16925
+ case TYPE.directory: {
16926
+ const rel = safeEntryPath(rawName, stripComponents);
16927
+ if (rel === null) continue;
16928
+ const full = resolve4(root, rel);
16929
+ if (full !== root && !full.startsWith(root + sep)) {
16930
+ throw new TarError(`Tar entry escapes extraction root: ${rawName}`);
16931
+ }
16932
+ await mkdir(full, { recursive: true });
16933
+ continue;
16934
+ }
16935
+ default:
16936
+ throw new TarError(
16937
+ `Refusing tar entry of type '${typeflag}' (${rawName}) \u2014 only regular files and directories are extracted`
16938
+ );
16939
+ }
16940
+ }
16941
+ if (filesWritten === 0) throw new TarError(`Archive contained no files under ${join3(dest)}`);
16942
+ return filesWritten;
16943
+ }
16944
+ function defaultCacheDir(platform = process.platform, env = process.env) {
16945
+ const parts = ["pglite-migrate", "engines"];
16946
+ if (platform === "win32") {
16947
+ const local = env.LOCALAPPDATA ?? join4(homedir(), "AppData", "Local");
16948
+ return join4(local, ...parts);
16949
+ }
16950
+ if (platform === "darwin") {
16951
+ return join4(homedir(), "Library", "Caches", ...parts);
16952
+ }
16953
+ const xdg = env.XDG_CACHE_HOME ?? join4(homedir(), ".cache");
16954
+ return join4(xdg, ...parts);
16955
+ }
16956
+ function tarballUrl(version2, registryUrl) {
16957
+ const unscoped = PGLITE_PACKAGE.split("/").pop() ?? PGLITE_PACKAGE;
16958
+ return `${registryUrl.replace(/\/+$/, "")}/${PGLITE_PACKAGE}/-/${unscoped}-${version2}.tgz`;
16959
+ }
16960
+ async function download(url2, version2) {
16961
+ let response;
16962
+ try {
16963
+ response = await fetch(url2);
16964
+ } catch (err) {
16965
+ throw new EngineFetchError(
16966
+ `Could not reach the npm registry to download ${PGLITE_PACKAGE}@${version2} (${url2}). If this machine is offline or behind a proxy, install the engine yourself instead: npm install pglite-old@npm:${PGLITE_PACKAGE}@${version2}`,
16967
+ { cause: err }
16968
+ );
16969
+ }
16970
+ if (!response.ok) {
16971
+ throw new EngineFetchError(
16972
+ `Registry returned ${response.status.toString()} ${response.statusText} for ${url2}`
16973
+ );
16974
+ }
16975
+ return Buffer.from(await response.arrayBuffer());
16976
+ }
16977
+ function verifyIntegrity(bytes, release) {
16978
+ const actual = `sha512-${createHash2("sha512").update(bytes).digest("base64")}`;
16979
+ if (actual !== release.integrity) {
16980
+ throw new IntegrityError(release.integrity, actual, release.version);
16981
+ }
16982
+ }
16983
+ function manifestEntryCandidates(manifest) {
16984
+ const candidates = [];
16985
+ const dot = manifest.exports?.["."];
16986
+ if (typeof dot === "string") {
16987
+ candidates.push(dot);
16988
+ } else if (dot !== null && typeof dot === "object") {
16989
+ const imp = dot.import;
16990
+ if (typeof imp === "string") candidates.push(imp);
16991
+ }
16992
+ if (typeof manifest.module === "string") candidates.push(manifest.module);
16993
+ if (typeof manifest.main === "string") candidates.push(manifest.main);
16994
+ candidates.push(FALLBACK_ENTRY);
16995
+ return candidates;
16996
+ }
16997
+ async function resolveEntry(dir) {
16998
+ const root = resolve22(dir);
16999
+ let manifest = {};
17000
+ try {
17001
+ const raw2 = JSON.parse(await readFile3(join4(root, "package.json"), "utf8"));
17002
+ if (raw2 !== null && typeof raw2 === "object") manifest = raw2;
17003
+ } catch {
17004
+ }
17005
+ for (const candidate of manifestEntryCandidates(manifest)) {
17006
+ const full = resolve22(root, candidate);
17007
+ if (full !== root && !full.startsWith(root + sep2)) continue;
17008
+ if (await exists(full)) return full;
17009
+ }
17010
+ throw new EngineFetchError(
17011
+ `Extracted engine at ${root} has no usable module entry point (tried the package manifest and ${FALLBACK_ENTRY})`
17012
+ );
17013
+ }
17014
+ async function acquireEngine(major, options = {}) {
17015
+ return acquireRelease(resolveEngine(major), options);
17016
+ }
17017
+ async function acquireRelease(release, options = {}) {
17018
+ const mode = options.cache ?? "keep";
17019
+ const registryUrl = options.registryUrl ?? DEFAULT_REGISTRY;
17020
+ if (mode === "ephemeral") {
17021
+ const parent = options.cacheDir ?? tmpdir();
17022
+ const dir2 = join4(parent, `pglite-migrate-engine-${release.version}-${randomUUID()}`);
17023
+ const bytes2 = await download(tarballUrl(release.version, registryUrl), release.version);
17024
+ verifyIntegrity(bytes2, release);
17025
+ await extractTarGz(bytes2, dir2, 1);
17026
+ return {
17027
+ entry: await resolveEntry(dir2),
17028
+ dir: dir2,
17029
+ release,
17030
+ fromCache: false,
17031
+ cleanup: () => rm2(dir2, { recursive: true, force: true })
17032
+ };
17033
+ }
17034
+ const root = options.cacheDir ?? defaultCacheDir();
17035
+ const dir = join4(root, `pglite-${release.version}`);
17036
+ const noop = () => Promise.resolve();
17037
+ if (await exists(dir)) {
17038
+ return { entry: await resolveEntry(dir), dir, release, fromCache: true, cleanup: noop };
17039
+ }
17040
+ const bytes = await download(tarballUrl(release.version, registryUrl), release.version);
17041
+ verifyIntegrity(bytes, release);
17042
+ await mkdir2(root, { recursive: true });
17043
+ const staging = join4(root, `.staging-${release.version}-${randomUUID()}`);
17044
+ try {
17045
+ await extractTarGz(bytes, staging, 1);
17046
+ try {
17047
+ await rename2(staging, dir);
17048
+ } catch (err) {
17049
+ if (!await exists(dir)) throw err;
17050
+ }
17051
+ } finally {
17052
+ await rm2(staging, { recursive: true, force: true });
17053
+ }
17054
+ return { entry: await resolveEntry(dir), dir, release, fromCache: false, cleanup: noop };
17055
+ }
17056
+ async function dirStats(dir) {
17057
+ let files = 0;
17058
+ let bytes = 0;
17059
+ const entries = await readdir(dir, { withFileTypes: true });
17060
+ for (const entry of entries) {
17061
+ const full = join5(dir, entry.name);
17062
+ if (entry.isDirectory()) {
17063
+ const sub = await dirStats(full);
17064
+ files += sub.files;
17065
+ bytes += sub.bytes;
17066
+ } else if (entry.isFile()) {
17067
+ files += 1;
17068
+ bytes += (await stat2(full)).size;
17069
+ }
17070
+ }
17071
+ return { files, bytes };
17072
+ }
17073
+ async function backupDataDir(dataDir, options = {}) {
17074
+ const timestamp2 = sanitizedTimestamp(options.timestamp);
17075
+ const backupDir = options.backupDir ?? `${dataDir}.bak-${timestamp2}`;
17076
+ const partial2 = `${backupDir}.partial`;
17077
+ if (await exists(backupDir)) {
17078
+ throw new Error(`Backup directory already exists: ${backupDir}`);
17079
+ }
17080
+ await rm(partial2, { recursive: true, force: true });
17081
+ await cp(dataDir, partial2, { recursive: true });
17082
+ await rename(partial2, backupDir);
17083
+ const sourceVersion = await readFile(join5(dataDir, "PG_VERSION"), "utf8").catch(() => null);
17084
+ if (sourceVersion !== null) {
17085
+ const backupVersion = await readFile(join5(backupDir, "PG_VERSION"), "utf8").catch(() => null);
17086
+ if (backupVersion === null || backupVersion.trim() !== sourceVersion.trim()) {
17087
+ throw new Error(`Backup verification failed: PG_VERSION mismatch for ${backupDir}`);
17088
+ }
17089
+ }
17090
+ const src = await dirStats(dataDir);
17091
+ const bak = await dirStats(backupDir);
17092
+ if (src.files !== bak.files || src.bytes !== bak.bytes) {
17093
+ throw new Error(
17094
+ `Backup verification failed: ${backupDir} has ${bak.files.toString()} files/${bak.bytes.toString()} bytes, source has ${src.files.toString()}/${src.bytes.toString()}`
17095
+ );
17096
+ }
17097
+ if (options.keep !== void 0) await pruneBackups(dataDir, backupDir, options.keep);
17098
+ return backupDir;
17099
+ }
17100
+ async function pruneBackups(dataDir, currentBackup, keep) {
17101
+ const parent = dirname5(dataDir);
17102
+ const prefix = `${basename2(dataDir)}.bak-`;
17103
+ const entries = await readdir(parent);
17104
+ const backups = entries.filter((e) => e.startsWith(prefix) && !e.endsWith(".partial")).sort();
17105
+ const current = basename2(currentBackup);
17106
+ const excess = backups.slice(0, Math.max(0, backups.length - Math.max(0, keep)));
17107
+ for (const name of excess) {
17108
+ if (name === current) continue;
17109
+ await rm(join5(parent, name), { recursive: true, force: true });
17110
+ }
17111
+ }
17112
+ function quoteIdent(name) {
17113
+ return `"${name.replace(/"/g, '""')}"`;
17114
+ }
17115
+ function quoteQualified(schema, name) {
17116
+ return `${quoteIdent(schema)}.${quoteIdent(name)}`;
17117
+ }
17118
+ function quoteLiteral(value) {
17119
+ return `'${value.replace(/'/g, "''")}'`;
17120
+ }
17121
+ function tableKey(t) {
17122
+ return `${t.schema}.${t.name}`;
17123
+ }
17124
+ function objectKey(schema, table, name) {
17125
+ return `${schema}.${table}.${name}`;
17126
+ }
17127
+ function systemSchemaFilter(alias = "nspname") {
17128
+ return `${alias} NOT IN ('pg_catalog', 'information_schema') AND ${alias} NOT LIKE 'pg_toast%' AND ${alias} NOT LIKE 'pg_temp%'`;
17129
+ }
17130
+ function regclassLiteral(schema, name) {
17131
+ return quoteLiteral(quoteQualified(schema, name));
17132
+ }
17133
+ async function tableKeys(db2) {
17134
+ const { rows } = await db2.query(
17135
+ `SELECT n.nspname || '.' || c.relname AS key
17136
+ FROM pg_class c
17137
+ JOIN pg_namespace n ON n.oid = c.relnamespace
17138
+ WHERE c.relkind = 'r' AND ${systemSchemaFilter("n.nspname")}`
17139
+ );
17140
+ return rows.map((r) => r.key);
17141
+ }
17142
+ async function countRows(db2, qualified) {
17143
+ const { rows } = await db2.query(`SELECT count(*)::text AS n FROM ${qualified}`);
17144
+ if (rows.length === 0) return 0;
17145
+ const raw2 = rows[0].n;
17146
+ const n = Number(raw2);
17147
+ if (!Number.isFinite(n)) throw new Error(`Unparseable row count for ${qualified}: ${raw2}`);
17148
+ return n;
17149
+ }
17150
+ async function hasRows(db2, qualified) {
17151
+ const { rows } = await db2.query(`SELECT 1 FROM ${qualified} LIMIT 1`);
17152
+ return rows.length > 0;
17153
+ }
17154
+ async function introspectSchema(db2) {
17155
+ const tables = await introspectTables(db2);
17156
+ const foreignKeys = await introspectForeignKeys(db2);
17157
+ const sequences = await introspectSequences(db2);
17158
+ return { tables, foreignKeys, sequences };
17159
+ }
17160
+ async function introspectTables(db2) {
17161
+ const { rows } = await db2.query(
17162
+ `SELECT n.nspname AS schema, c.relname AS name
17163
+ FROM pg_class c
17164
+ JOIN pg_namespace n ON n.oid = c.relnamespace
17165
+ WHERE c.relkind = 'r' AND ${systemSchemaFilter("n.nspname")}
17166
+ ORDER BY n.nspname, c.relname`
17167
+ );
17168
+ const tables = [];
17169
+ for (const { schema, name } of rows) {
17170
+ tables.push({ schema, name, columns: await introspectColumns(db2, schema, name) });
17171
+ }
17172
+ return tables;
17173
+ }
17174
+ async function introspectColumns(db2, schema, table) {
17175
+ const regclass = regclassLiteral(schema, table);
17176
+ const { rows } = await db2.query(
17177
+ `SELECT a.attname AS name,
17178
+ format_type(a.atttypid, a.atttypmod) AS type,
17179
+ a.attgenerated AS generated,
17180
+ a.attidentity AS identity
17181
+ FROM pg_attribute a
17182
+ WHERE a.attrelid = ${regclass}::regclass
17183
+ AND a.attnum > 0 AND NOT a.attisdropped
17184
+ ORDER BY a.attnum`
17185
+ );
17186
+ return rows.map((r) => ({
17187
+ name: r.name,
17188
+ type: r.type,
17189
+ // attgenerated is 's' for STORED generated columns, '' otherwise.
17190
+ generated: r.generated === "s",
17191
+ // attidentity is 'a' (always) / 'd' (by default) / '' (not an identity column).
17192
+ identity: r.identity === "a" ? "always" : r.identity === "d" ? "default" : null
17193
+ }));
17194
+ }
17195
+ async function introspectForeignKeys(db2) {
17196
+ const { rows } = await db2.query(
17197
+ `SELECT cn.nspname || '.' || cc.relname AS child,
17198
+ pn.nspname || '.' || pc.relname AS parent
17199
+ FROM pg_constraint con
17200
+ JOIN pg_class cc ON cc.oid = con.conrelid
17201
+ JOIN pg_namespace cn ON cn.oid = cc.relnamespace
17202
+ JOIN pg_class pc ON pc.oid = con.confrelid
17203
+ JOIN pg_namespace pn ON pn.oid = pc.relnamespace
17204
+ WHERE con.contype = 'f'
17205
+ AND con.conrelid <> con.confrelid
17206
+ AND ${systemSchemaFilter("cn.nspname")}`
17207
+ );
17208
+ return rows.map((r) => ({ child: r.child, parent: r.parent }));
17209
+ }
17210
+ async function introspectSequences(db2) {
17211
+ const { rows } = await db2.query(
17212
+ `SELECT schemaname, sequencename, last_value
17213
+ FROM pg_sequences
17214
+ WHERE ${systemSchemaFilter("schemaname")}`
17215
+ );
17216
+ return rows.map((r) => ({
17217
+ schema: r.schemaname,
17218
+ name: r.sequencename,
17219
+ lastValue: r.last_value
17220
+ }));
17221
+ }
17222
+ async function readClusterVersion(dataDir) {
17223
+ const file2 = join22(dataDir, "PG_VERSION");
17224
+ const raw2 = await readFile2(file2, "utf8");
17225
+ const major = Number.parseInt(raw2.trim(), 10);
17226
+ if (!Number.isInteger(major)) {
17227
+ throw new Error(`Could not parse a major version from ${file2} (got ${JSON.stringify(raw2)})`);
17228
+ }
17229
+ return major;
17230
+ }
17231
+ function isUnresolvedModule(err, specifier) {
17232
+ if (!(err instanceof Error)) return false;
17233
+ const code = errorCode(err);
17234
+ if (code === void 0 || !UNRESOLVED_CODES.has(code)) return false;
17235
+ return err.message.includes(specifier);
17236
+ }
17237
+ async function importEngine(modulePath) {
17238
+ const specifier = isAbsolute2(modulePath) ? pathToFileURL(modulePath).href : modulePath;
17239
+ return import(specifier);
17240
+ }
17241
+ function constructorFrom(mod, modulePath) {
17242
+ const PGlite3 = mod.PGlite;
17243
+ if (typeof PGlite3 !== "function") {
17244
+ throw new Error(`Module ${modulePath} does not export a PGlite constructor`);
17245
+ }
17246
+ return PGlite3;
17247
+ }
17248
+ function construct(mod, modulePath, dataDir, pgliteOptions) {
17249
+ const Engine = constructorFrom(mod, modulePath);
17250
+ return pgliteOptions === void 0 ? new Engine(dataDir) : new Engine(dataDir, pgliteOptions);
17251
+ }
17252
+ async function missingEngineError(modulePath, dataDir, cause) {
17253
+ const major = await readClusterVersion(dataDir).catch(() => null);
17254
+ const lines = [`Could not load the PGlite engine "${modulePath}".`];
17255
+ if (major !== null) {
17256
+ lines.push(`${dataDir} is a PostgreSQL ${major.toString()} data directory.`);
17257
+ const pinned = tryResolveEngine(major)?.version ?? null;
17258
+ if (pinned !== null) {
17259
+ lines.push(
17260
+ `Install a matching engine:`,
17261
+ ` npm install ${modulePath}@npm:@electric-sql/pglite@${pinned}`,
17262
+ `\u2026or pass fetchMissingEngine (CLI: --fetch-missing-engine) to download it automatically.`
17263
+ );
17264
+ } else {
17265
+ lines.push(`No engine is pinned for PostgreSQL ${major.toString()}; install one yourself.`);
17266
+ }
17267
+ } else {
17268
+ lines.push(`Install it, or pass fetchMissingEngine to download a pinned engine automatically.`);
17269
+ }
17270
+ return new Error(lines.join("\n"), { cause });
17271
+ }
17272
+ async function openDataDir(dataDir, modulePath = "@electric-sql/pglite", options = {}) {
17273
+ let mod;
17274
+ try {
17275
+ mod = await importEngine(modulePath);
17276
+ } catch (err) {
17277
+ if (!isUnresolvedModule(err, modulePath)) throw err;
17278
+ if (options.fetchMissingEngine !== true) {
17279
+ throw await missingEngineError(modulePath, dataDir, err);
17280
+ }
17281
+ return openAcquired(dataDir, options);
17282
+ }
17283
+ return construct(mod, modulePath, dataDir, options.pgliteOptions);
17284
+ }
17285
+ async function openAcquired(dataDir, options) {
17286
+ const { acquireEngine: acquireEngine22, acquireRelease: acquireRelease22 } = await Promise.resolve().then(() => (init_acquire(), acquire_exports));
17287
+ const acquireOptions = {};
17288
+ if (options.cache !== void 0) acquireOptions.cache = options.cache;
17289
+ if (options.cacheDir !== void 0) acquireOptions.cacheDir = options.cacheDir;
17290
+ if (options.registryUrl !== void 0) acquireOptions.registryUrl = options.registryUrl;
17291
+ const engine = options.release !== void 0 ? await acquireRelease22(options.release, acquireOptions) : await acquireEngine22(options.major ?? await readClusterVersion(dataDir), acquireOptions);
17292
+ const mod = await importEngine(engine.entry);
17293
+ const cluster = construct(mod, engine.entry, dataDir, options.pgliteOptions);
17294
+ const close = cluster.close.bind(cluster);
17295
+ cluster.close = async () => {
17296
+ try {
17297
+ await close();
17298
+ } finally {
17299
+ await engine.cleanup();
17300
+ }
17301
+ };
17302
+ cluster.acquired = { version: engine.release.version, fromCache: engine.fromCache };
17303
+ return cluster;
17304
+ }
17305
+ function numericLiteral(value, field) {
17306
+ if (!/^-?\d+$/.test(value)) {
17307
+ throw new Error(`Refusing to reconstruct: non-numeric ${field} from the catalog: ${value}`);
17308
+ }
17309
+ return value;
17310
+ }
17311
+ async function reconstructSchema(source, target, options = {}) {
17312
+ const onUnsupported = options.onUnsupported ?? "warn";
17313
+ const unsupported = await detectUnsupported(source);
17314
+ if (onUnsupported === "error" && unsupported.length > 0) {
17315
+ const list = unsupported.map((u) => `${u.kind} ${u.name}`).join(", ");
17316
+ throw new Error(
17317
+ `Cannot reconstruct: source has ${unsupported.length.toString()} out-of-scope object(s) that would not be recreated: ${list}. Re-run with onUnsupported: 'warn' to rebuild the app-class schema anyway.`
17318
+ );
17319
+ }
17320
+ const schemas = await reconstructSchemas(source, target);
17321
+ const { enums, domains, composites, ranges } = await reconstructCustomTypes(source, target);
17322
+ const sequences = await reconstructSequences(source, target);
17323
+ const tables = await reconstructTables(source, target);
17324
+ await reconstructSequenceOwnership(source, target);
17325
+ const constraints = await reconstructConstraints(source, target);
17326
+ const indexes = await reconstructIndexes(source, target);
17327
+ return {
17328
+ schemas,
17329
+ enums,
17330
+ domains,
17331
+ composites,
17332
+ ranges,
17333
+ sequences,
17334
+ tables,
17335
+ constraints,
17336
+ indexes,
17337
+ unsupported
17338
+ };
17339
+ }
17340
+ async function reconstructSchemas(source, target) {
17341
+ const { rows } = await source.query(
17342
+ `SELECT n.nspname AS name
17343
+ FROM pg_namespace n
17344
+ WHERE ${SYS} AND n.nspname <> 'public'
17345
+ ORDER BY n.nspname`
17346
+ );
17347
+ const created = [];
17348
+ for (const r of rows) {
17349
+ await target.exec(`CREATE SCHEMA IF NOT EXISTS ${quoteIdent(r.name)}`);
17350
+ created.push(r.name);
17351
+ }
17352
+ return created;
17353
+ }
17354
+ async function reconstructCustomTypes(source, target) {
17355
+ const { rows } = await source.query(
17356
+ `SELECT t.oid::int AS oid, n.nspname AS schema, t.typname AS name, t.typtype AS kind,
17357
+ CASE WHEN t.typtype = 'e' THEN (
17358
+ SELECT array_agg(e.enumlabel ORDER BY e.enumsortorder)
17359
+ FROM pg_enum e WHERE e.enumtypid = t.oid
17360
+ ) END AS labels,
17361
+ CASE WHEN t.typtype = 'd'
17362
+ THEN format_type(t.typbasetype, t.typtypmod) END AS base,
17363
+ t.typnotnull AS notnull,
17364
+ pg_get_expr(t.typdefaultbin, 0) AS default_expr,
17365
+ co.collname AS collation,
17366
+ cn.nspname AS collation_schema,
17367
+ CASE WHEN t.typtype = 'r'
17368
+ THEN format_type(rng.rngsubtype, NULL) END AS range_subtype,
17369
+ rco.collname AS range_collation,
17370
+ rcn.nspname AS range_collation_schema,
17371
+ CASE WHEN opc.opcdefault IS FALSE THEN opc.opcname END AS range_subopclass,
17372
+ CASE WHEN opc.opcdefault IS FALSE THEN opcn.nspname END AS range_subopclass_schema,
17373
+ mrt.typname AS range_multirange
17374
+ FROM pg_type t
17375
+ JOIN pg_namespace n ON n.oid = t.typnamespace
17376
+ LEFT JOIN pg_collation co ON co.oid = t.typcollation AND t.typtype = 'd'
17377
+ LEFT JOIN pg_namespace cn ON cn.oid = co.collnamespace
17378
+ LEFT JOIN pg_range rng ON rng.rngtypid = t.oid
17379
+ LEFT JOIN pg_collation rco ON rco.oid = rng.rngcollation
17380
+ LEFT JOIN pg_namespace rcn ON rcn.oid = rco.collnamespace
17381
+ LEFT JOIN pg_opclass opc ON opc.oid = rng.rngsubopc
17382
+ LEFT JOIN pg_namespace opcn ON opcn.oid = opc.opcnamespace
17383
+ LEFT JOIN pg_type mrt ON mrt.oid = rng.rngmultitypid
17384
+ WHERE ${SYS}
17385
+ -- 'm' (multirange) is deliberately absent: Postgres creates it
17386
+ -- implicitly with its range, so emitting one would fail (FR-17.6).
17387
+ AND t.typtype IN ('e', 'd', 'c', 'r')
17388
+ -- A range needing a canonical/subdiff function cannot be emitted in one
17389
+ -- statement and stays reported instead (FR-17.2).
17390
+ AND (t.typtype <> 'r' OR (rng.rngcanonical = 0 AND rng.rngsubdiff = 0))
17391
+ -- A standalone composite has a pg_class entry of relkind 'c'; a table's
17392
+ -- implicit row type points at the table itself and must not be emitted.
17393
+ AND (t.typtype <> 'c'
17394
+ OR EXISTS (SELECT 1 FROM pg_class c
17395
+ WHERE c.oid = t.typrelid AND c.relkind = 'c'))
17396
+ ORDER BY t.oid`
17397
+ );
17398
+ const checks = await domainChecks(source);
17399
+ const out = { enums: [], domains: [], composites: [], ranges: [] };
17400
+ for (const t of rows) {
17401
+ const qualified = quoteQualified(t.schema, t.name);
17402
+ if (t.kind === "e") {
17403
+ const labels = (t.labels ?? []).map((l) => quoteLiteral(l)).join(", ");
17404
+ await target.exec(`CREATE TYPE ${qualified} AS ENUM (${labels})`);
17405
+ out.enums.push(tableKey(t));
17406
+ } else if (t.kind === "d") {
17407
+ await target.exec(domainDef(qualified, t, checks.get(t.oid) ?? []));
17408
+ out.domains.push(tableKey(t));
17409
+ } else if (t.kind === "c") {
17410
+ await target.exec(`CREATE TYPE ${qualified} AS (${await compositeAttrs(source, t.oid)})`);
17411
+ out.composites.push(tableKey(t));
17412
+ } else {
17413
+ await target.exec(rangeDef(qualified, t));
17414
+ out.ranges.push(tableKey(t));
17415
+ }
17416
+ }
17417
+ return out;
17418
+ }
17419
+ async function domainChecks(source) {
17420
+ const { rows } = await source.query(
17421
+ `SELECT t.oid::int AS oid, con.conname AS name, pg_get_constraintdef(con.oid) AS def
17422
+ FROM pg_constraint con
17423
+ JOIN pg_type t ON t.oid = con.contypid
17424
+ JOIN pg_namespace n ON n.oid = t.typnamespace
17425
+ WHERE con.contype = 'c' AND ${SYS}
17426
+ ORDER BY t.oid, con.conname`
17427
+ );
17428
+ const byType = /* @__PURE__ */ new Map();
17429
+ for (const r of rows) {
17430
+ const clause = `CONSTRAINT ${quoteIdent(r.name)} ${r.def}`;
17431
+ byType.set(r.oid, [...byType.get(r.oid) ?? [], clause]);
17432
+ }
17433
+ return byType;
17434
+ }
17435
+ function domainDef(qualified, t, checks) {
17436
+ const parts = [`CREATE DOMAIN ${qualified} AS ${t.base ?? "text"}`];
17437
+ if (t.collation !== null && t.collation_schema !== null) {
17438
+ parts.push(`COLLATE ${quoteQualified(t.collation_schema, t.collation)}`);
17439
+ }
17440
+ if (t.default_expr !== null) parts.push(`DEFAULT ${t.default_expr}`);
17441
+ if (t.notnull) parts.push("NOT NULL");
17442
+ parts.push(...checks);
17443
+ return parts.join(" ");
17444
+ }
17445
+ function rangeDef(qualified, t) {
17446
+ const opts = [`subtype = ${t.range_subtype ?? "text"}`];
17447
+ if (t.range_collation !== null && t.range_collation_schema !== null) {
17448
+ opts.push(`collation = ${quoteQualified(t.range_collation_schema, t.range_collation)}`);
17449
+ }
17450
+ if (t.range_subopclass !== null && t.range_subopclass_schema !== null) {
17451
+ opts.push(
17452
+ `subtype_opclass = ${quoteQualified(t.range_subopclass_schema, t.range_subopclass)}`
17453
+ );
17454
+ }
17455
+ if (t.range_multirange !== null) {
17456
+ opts.push(`multirange_type_name = ${quoteIdent(t.range_multirange)}`);
17457
+ }
17458
+ return `CREATE TYPE ${qualified} AS RANGE (${opts.join(", ")})`;
17459
+ }
17460
+ async function compositeAttrs(source, typeOid) {
17461
+ const { rows } = await source.query(
17462
+ `SELECT a.attname AS name, format_type(a.atttypid, a.atttypmod) AS type
17463
+ FROM pg_type t
17464
+ JOIN pg_attribute a ON a.attrelid = t.typrelid
17465
+ WHERE t.oid = ${typeOid.toString()}
17466
+ AND a.attnum > 0 AND NOT a.attisdropped
17467
+ ORDER BY a.attnum`
17468
+ );
17469
+ return rows.map((r) => `${quoteIdent(r.name)} ${r.type}`).join(", ");
17470
+ }
17471
+ async function reconstructSequences(source, target) {
17472
+ const { rows } = await source.query(
17473
+ `SELECT n.nspname AS schema, c.relname AS name,
17474
+ format_type(s.seqtypid, NULL) AS data_type,
17475
+ s.seqstart::text AS start_value,
17476
+ s.seqincrement::text AS increment_by,
17477
+ s.seqmin::text AS min_value,
17478
+ s.seqmax::text AS max_value,
17479
+ s.seqcycle AS cycle
17480
+ FROM pg_class c
17481
+ JOIN pg_namespace n ON n.oid = c.relnamespace
17482
+ JOIN pg_sequence s ON s.seqrelid = c.oid
17483
+ WHERE c.relkind = 'S' AND ${SYS}
17484
+ AND NOT EXISTS (SELECT 1 FROM pg_depend d WHERE d.objid = c.oid AND d.deptype = 'i')
17485
+ ORDER BY n.nspname, c.relname`
17486
+ );
17487
+ const created = [];
17488
+ for (const r of rows) {
17489
+ await target.exec(
17490
+ `CREATE SEQUENCE IF NOT EXISTS ${quoteQualified(r.schema, r.name)} AS ${r.data_type} INCREMENT BY ${numericLiteral(r.increment_by, "sequence increment")} MINVALUE ${numericLiteral(r.min_value, "sequence minvalue")} MAXVALUE ${numericLiteral(r.max_value, "sequence maxvalue")} START WITH ${numericLiteral(r.start_value, "sequence start")}` + (r.cycle ? " CYCLE" : " NO CYCLE")
17491
+ );
17492
+ created.push(tableKey(r));
17493
+ }
17494
+ return created;
17495
+ }
17496
+ async function reconstructSequenceOwnership(source, target) {
17497
+ const { rows } = await source.query(
17498
+ `SELECT n.nspname AS seq_schema, c.relname AS seq_name,
17499
+ tn.nspname AS tbl_schema, tc.relname AS tbl_name, a.attname AS col
17500
+ FROM pg_depend d
17501
+ JOIN pg_class c ON c.oid = d.objid AND c.relkind = 'S'
17502
+ JOIN pg_namespace n ON n.oid = c.relnamespace
17503
+ JOIN pg_class tc ON tc.oid = d.refobjid
17504
+ JOIN pg_namespace tn ON tn.oid = tc.relnamespace
17505
+ JOIN pg_attribute a ON a.attrelid = d.refobjid AND a.attnum = d.refobjsubid
17506
+ WHERE d.classid = 'pg_class'::regclass AND d.deptype = 'a' AND ${SYS}`
17507
+ );
17508
+ for (const r of rows) {
17509
+ await target.exec(
17510
+ `ALTER SEQUENCE ${quoteQualified(r.seq_schema, r.seq_name)} OWNED BY ${quoteQualified(r.tbl_schema, r.tbl_name)}.${quoteIdent(r.col)}`
17511
+ );
17512
+ }
17513
+ }
17514
+ async function reconstructTables(source, target) {
17515
+ const { rows: tables } = await source.query(
17516
+ `SELECT n.nspname AS schema, c.relname AS name
17517
+ FROM pg_class c
17518
+ JOIN pg_namespace n ON n.oid = c.relnamespace
17519
+ WHERE c.relkind = 'r' AND ${SYS}
17520
+ ORDER BY n.nspname, c.relname`
17521
+ );
17522
+ const created = [];
17523
+ for (const t of tables) {
17524
+ const qualified = quoteQualified(t.schema, t.name);
17525
+ const { rows: cols } = await source.query(
17526
+ `SELECT a.attname AS name,
17527
+ format_type(a.atttypid, a.atttypmod) AS type,
17528
+ a.attnotnull AS notnull,
17529
+ a.attidentity AS identity,
17530
+ a.attgenerated AS generated,
17531
+ pg_get_expr(ad.adbin, ad.adrelid) AS default_expr
17532
+ FROM pg_attribute a
17533
+ LEFT JOIN pg_attrdef ad ON ad.adrelid = a.attrelid AND ad.adnum = a.attnum
17534
+ WHERE a.attrelid = ${regclassLiteral(t.schema, t.name)}::regclass
17535
+ AND a.attnum > 0 AND NOT a.attisdropped
17536
+ ORDER BY a.attnum`
17537
+ );
17538
+ const defs = cols.map((c) => columnDef(c)).join(",\n ");
17539
+ await target.exec(`CREATE TABLE ${qualified} (
17540
+ ${defs}
17541
+ )`);
17542
+ created.push(tableKey(t));
17543
+ }
17544
+ return created;
17545
+ }
17546
+ function columnDef(c) {
17547
+ const parts = [`${quoteIdent(c.name)} ${c.type}`];
17548
+ if (c.identity === "a") parts.push("GENERATED ALWAYS AS IDENTITY");
17549
+ else if (c.identity === "d") parts.push("GENERATED BY DEFAULT AS IDENTITY");
17550
+ else if (c.generated === "s" && c.default_expr !== null) {
17551
+ parts.push(`GENERATED ALWAYS AS (${c.default_expr}) STORED`);
17552
+ } else if (c.default_expr !== null) {
17553
+ parts.push(`DEFAULT ${c.default_expr}`);
17554
+ }
17555
+ if (c.notnull && c.identity === "") parts.push("NOT NULL");
17556
+ return parts.join(" ");
17557
+ }
17558
+ async function reconstructConstraints(source, target) {
17559
+ const { rows } = await source.query(
17560
+ `SELECT n.nspname AS schema, c.relname AS table, con.conname AS name,
17561
+ con.contype AS contype, pg_get_constraintdef(con.oid) AS def
17562
+ FROM pg_constraint con
17563
+ JOIN pg_class c ON c.oid = con.conrelid
17564
+ JOIN pg_namespace n ON n.oid = c.relnamespace
17565
+ WHERE con.contype IN ('p', 'u', 'c', 'f') AND ${SYS}
17566
+ ORDER BY CASE con.contype WHEN 'f' THEN 1 ELSE 0 END`
17567
+ // FKs after PK/UNIQUE/CHECK
17568
+ );
17569
+ const created = [];
17570
+ for (const r of rows) {
17571
+ await target.exec(
17572
+ `ALTER TABLE ${quoteQualified(r.schema, r.table)} ADD CONSTRAINT ${quoteIdent(r.name)} ${r.def}`
17573
+ );
17574
+ created.push(objectKey(r.schema, r.table, r.name));
17575
+ }
17576
+ return created;
17577
+ }
17578
+ async function reconstructIndexes(source, target) {
17579
+ const { rows } = await source.query(
17580
+ `SELECT ic.relname AS name, pg_get_indexdef(i.indexrelid) AS def
17581
+ FROM pg_index i
17582
+ JOIN pg_class ic ON ic.oid = i.indexrelid
17583
+ JOIN pg_class tc ON tc.oid = i.indrelid
17584
+ JOIN pg_namespace n ON n.oid = tc.relnamespace
17585
+ WHERE ${SYS}
17586
+ AND NOT EXISTS (SELECT 1 FROM pg_constraint con WHERE con.conindid = i.indexrelid)
17587
+ ORDER BY ic.relname`
17588
+ );
17589
+ const created = [];
17590
+ for (const r of rows) {
17591
+ await target.exec(r.def);
17592
+ created.push(r.name);
17593
+ }
17594
+ return created;
17595
+ }
17596
+ async function detectUnsupported(source) {
17597
+ const found = [];
17598
+ for (const detector of DETECTORS) {
17599
+ const { rows } = await source.query(detector.sql);
17600
+ for (const r of rows) {
17601
+ found.push({ kind: r.kind ?? detector.label, name: r.name });
17602
+ }
17603
+ }
17604
+ return found;
17605
+ }
17606
+ function topologicalSort(tables, foreignKeys) {
17607
+ const keys = new Set(tables.map(tableKey));
17608
+ const dependsOn = /* @__PURE__ */ new Map();
17609
+ for (const t of tables) dependsOn.set(tableKey(t), /* @__PURE__ */ new Set());
17610
+ for (const fk of foreignKeys) {
17611
+ if (keys.has(fk.child) && keys.has(fk.parent) && fk.child !== fk.parent) {
17612
+ dependsOn.get(fk.child)?.add(fk.parent);
17613
+ }
17614
+ }
17615
+ const ordered = [];
17616
+ const placed = /* @__PURE__ */ new Set();
17617
+ const byKey = new Map(tables.map((t) => [tableKey(t), t]));
17618
+ let progress = true;
17619
+ while (placed.size < tables.length && progress) {
17620
+ progress = false;
17621
+ for (const t of tables) {
17622
+ const key = tableKey(t);
17623
+ if (placed.has(key)) continue;
17624
+ const parents = dependsOn.get(key);
17625
+ if (parents && [...parents].every((p) => placed.has(p))) {
17626
+ ordered.push(t);
17627
+ placed.add(key);
17628
+ progress = true;
17629
+ }
17630
+ }
17631
+ }
17632
+ const cycles = [];
17633
+ for (const t of tables) {
17634
+ const key = tableKey(t);
17635
+ if (!placed.has(key)) {
17636
+ cycles.push(key);
17637
+ const table = byKey.get(key);
17638
+ if (table) ordered.push(table);
17639
+ }
17640
+ }
17641
+ return { ordered, cycles };
17642
+ }
17643
+ async function transferTable(source, target, table, onProgress) {
17644
+ const qualified = quoteQualified(table.schema, table.name);
17645
+ const cols = table.columns.filter((c) => c.generated !== true);
17646
+ const colList = cols.map((c) => quoteIdent(c.name)).join(", ");
17647
+ let result;
17648
+ try {
17649
+ const rowsCopied = await copyTable(source, target, qualified, colList);
17650
+ result = { table: tableKey(table), rowsCopied, method: "copy" };
17651
+ } catch (copyErr) {
17652
+ try {
17653
+ const rowsCopied = await insertTable(source, target, qualified, cols, colList);
17654
+ result = {
17655
+ table: tableKey(table),
17656
+ rowsCopied,
17657
+ method: "insert",
17658
+ fallbackReason: message(copyErr)
17659
+ };
17660
+ } catch (insertErr) {
17661
+ throw new Error(
17662
+ `Table ${tableKey(table)}: COPY failed (${message(copyErr)}), and the row-by-row INSERT fallback also failed (${message(insertErr)}).`,
17663
+ { cause: insertErr }
17664
+ );
17665
+ }
17666
+ }
17667
+ onProgress?.(result);
17668
+ return result;
17669
+ }
17670
+ function message(err) {
17671
+ return err instanceof Error ? err.message : String(err);
17672
+ }
17673
+ async function copyTable(source, target, qualified, colList) {
17674
+ if (colList === "") return 0;
17675
+ const rowCount2 = await countRows(source, qualified);
17676
+ if (rowCount2 === 0) return 0;
17677
+ const { blob } = await source.query(`COPY ${qualified} (${colList}) TO '/dev/blob'`);
17678
+ if (blob === void 0) throw new Error("COPY TO produced no blob payload");
17679
+ await target.query(`COPY ${qualified} (${colList}) FROM '/dev/blob'`, [], { blob });
17680
+ return rowCount2;
17681
+ }
17682
+ async function insertTable(source, target, qualified, cols, colList) {
17683
+ if (colList === "") return 0;
17684
+ const { rows } = await source.query(`SELECT ${colList} FROM ${qualified}`);
17685
+ if (rows.length > 0) {
17686
+ const placeholders = cols.map((_, i) => `$${(i + 1).toString()}`).join(", ");
17687
+ const insertSql = `INSERT INTO ${qualified} (${colList}) VALUES (${placeholders})`;
17688
+ for (const row of rows) {
17689
+ const values = cols.map((c) => row[c.name] ?? null);
17690
+ await target.query(insertSql, values);
17691
+ }
17692
+ }
17693
+ return rows.length;
17694
+ }
17695
+ async function targetCycleConstraints(target, cyclicTables) {
17696
+ const constraints = [];
17697
+ for (const t of cyclicTables) {
17698
+ const qualified = quoteQualified(t.schema, t.name);
17699
+ const { rows } = await target.query(
17700
+ `SELECT conname AS name, condeferrable AS deferrable
17701
+ FROM pg_constraint
17702
+ WHERE contype = 'f' AND conrelid = ${regclassLiteral(t.schema, t.name)}::regclass`
17703
+ );
17704
+ for (const r of rows) constraints.push({ qualified, name: r.name, deferrable: r.deferrable });
17705
+ }
17706
+ return constraints;
17707
+ }
17708
+ async function transferCycle(source, target, cyclicTables, onProgress) {
17709
+ const toFlip = (await targetCycleConstraints(target, cyclicTables)).filter((c) => !c.deferrable);
17710
+ const flipped = [];
17711
+ const results = [];
17712
+ try {
17713
+ for (const c of toFlip) {
17714
+ await target.query(
17715
+ `ALTER TABLE ${c.qualified} ALTER CONSTRAINT ${quoteIdent(c.name)} DEFERRABLE INITIALLY IMMEDIATE`
17716
+ );
17717
+ flipped.push(c);
17718
+ }
17719
+ await target.exec("BEGIN");
17720
+ await target.exec("SET CONSTRAINTS ALL DEFERRED");
17721
+ for (const t of cyclicTables) {
17722
+ results.push(await transferTable(source, target, t, onProgress));
17723
+ }
17724
+ await target.exec("COMMIT");
17725
+ } catch (err) {
17726
+ await target.exec("ROLLBACK").catch(() => void 0);
17727
+ throw err;
17728
+ } finally {
17729
+ for (const c of flipped) {
17730
+ await target.query(`ALTER TABLE ${c.qualified} ALTER CONSTRAINT ${quoteIdent(c.name)} NOT DEFERRABLE`).catch(() => void 0);
17731
+ }
17732
+ }
17733
+ return results;
17734
+ }
17735
+ async function applySequences(target, sequences) {
17736
+ let applied = 0;
17737
+ for (const seq of sequences) {
17738
+ if (seq.lastValue === null) continue;
17739
+ const seqRef = regclassLiteral(seq.schema, seq.name);
17740
+ await target.query(`SELECT setval(${seqRef}::regclass, $1, true)`, [seq.lastValue.toString()]);
17741
+ applied++;
17742
+ }
17743
+ return applied;
17744
+ }
17745
+ async function tableDigest(db2, table, columns) {
17746
+ const qualified = quoteQualified(table.schema, table.name);
17747
+ const projection = columns.map(quoteIdent).join(", ");
17748
+ const { rows } = await db2.query(
17749
+ `SELECT md5(coalesce(string_agg(x::text, E'\\n' ORDER BY x::text), '')) AS d
17750
+ FROM (SELECT ${projection} FROM ${qualified}) AS x`
17751
+ );
17752
+ return rows[0]?.d ?? "";
17753
+ }
17754
+ function comparableColumns(source, targetColumns2) {
17755
+ const sourceNames = source.columns.map((c) => c.name);
17756
+ const sourceSet = new Set(sourceNames);
17757
+ return {
17758
+ compared: sourceNames.filter((n) => targetColumns2.has(n)).sort(),
17759
+ missing: sourceNames.filter((n) => !targetColumns2.has(n)).sort(),
17760
+ extra: [...targetColumns2].filter((n) => !sourceSet.has(n)).sort()
17761
+ };
17762
+ }
17763
+ async function targetTableMap(db2, level) {
17764
+ if (level !== "full") {
17765
+ return new Map((await tableKeys(db2)).map((key) => [key, null]));
17766
+ }
17767
+ const schema = await introspectSchema(db2);
17768
+ return new Map(schema.tables.map((t) => [tableKey(t), new Set(t.columns.map((c) => c.name))]));
17769
+ }
17770
+ async function sequenceValue(db2, schema, name) {
17771
+ const { rows } = await db2.query(
17772
+ `SELECT last_value AS v FROM pg_sequences
17773
+ WHERE schemaname = ${quoteLiteral(schema)} AND sequencename = ${quoteLiteral(name)}`
17774
+ );
17775
+ if (rows.length === 0) return null;
17776
+ const v = rows[0].v;
17777
+ return v === null ? null : v.toString();
17778
+ }
17779
+ async function validateMigration(source, target, schema, level) {
17780
+ const targetTables = await targetTableMap(target, level);
17781
+ const tables = [];
17782
+ for (const t of schema.tables) {
17783
+ const key = tableKey(t);
17784
+ const qualified = quoteQualified(t.schema, t.name);
17785
+ const sourceRows = await countRows(source, qualified);
17786
+ const targetColumns2 = targetTables.get(key);
17787
+ if (targetColumns2 === void 0) {
17788
+ tables.push({ table: key, sourceRows, targetRows: 0, missingTable: true, ok: false });
17789
+ continue;
17790
+ }
17791
+ const targetRows = await countRows(target, qualified);
17792
+ let ok2 = sourceRows === targetRows;
17793
+ const entry = { table: key, sourceRows, targetRows, ok: ok2 };
17794
+ if (targetColumns2 !== null) {
17795
+ const cols = comparableColumns(t, targetColumns2);
17796
+ entry.comparedColumns = cols.compared;
17797
+ if (cols.missing.length > 0) entry.missingColumns = cols.missing;
17798
+ if (cols.extra.length > 0) entry.extraColumns = cols.extra;
17799
+ if (cols.missing.length > 0) ok2 = false;
17800
+ else if (ok2 && cols.compared.length > 0) {
17801
+ entry.digestMatch = await tableDigest(source, t, cols.compared) === await tableDigest(target, t, cols.compared);
17802
+ ok2 = entry.digestMatch;
17803
+ }
17804
+ }
17805
+ entry.ok = ok2;
17806
+ tables.push(entry);
17807
+ }
17808
+ const sequences = [];
17809
+ for (const s of schema.sequences) {
17810
+ if (s.lastValue === null) continue;
17811
+ const sourceValue = s.lastValue.toString();
17812
+ const targetValue = await sequenceValue(target, s.schema, s.name);
17813
+ const ok2 = targetValue !== null && BigInt(targetValue) >= BigInt(sourceValue);
17814
+ sequences.push({ sequence: tableKey(s), sourceValue, targetValue, ok: ok2 });
17815
+ }
17816
+ const ok = tables.every((t) => t.ok) && sequences.every((s) => s.ok);
17817
+ return { level, ok, tables, sequences };
17818
+ }
17819
+ async function rowCount(db2, table) {
17820
+ return countRows(db2, quoteQualified(table.schema, table.name));
17821
+ }
17822
+ async function prepareTarget(target, ordered, onExisting) {
17823
+ const skip = /* @__PURE__ */ new Set();
17824
+ if (ordered.length === 0) return { skip, truncated: [] };
17825
+ const populated = [];
17826
+ for (const t of ordered) {
17827
+ if (await hasRows(target, quoteQualified(t.schema, t.name))) populated.push(tableKey(t));
17828
+ }
17829
+ if (onExisting === "truncate") {
17830
+ const list = ordered.map((t) => quoteQualified(t.schema, t.name)).join(", ");
17831
+ await target.exec(`TRUNCATE TABLE ${list}`);
17832
+ return { skip, truncated: populated };
17833
+ }
17834
+ if (onExisting === "skip") {
17835
+ for (const key of populated) skip.add(key);
17836
+ return { skip, truncated: [] };
17837
+ }
17838
+ if (populated.length > 0) {
17839
+ throw new Error(
17840
+ `Target already contains rows in: ${populated.join(", ")}. Re-run with onExisting: 'truncate' or 'skip', or start from an empty target.`
17841
+ );
17842
+ }
17843
+ return { skip, truncated: [] };
17844
+ }
17845
+ function fallbackWarning(result) {
17846
+ if (result.method === "insert" && result.fallbackReason !== void 0) {
17847
+ return `Table ${result.table}: COPY unavailable (${result.fallbackReason}); fell back to row-by-row INSERT.`;
17848
+ }
17849
+ return null;
17850
+ }
17851
+ async function planMigration(source, onProgress) {
17852
+ const schema = await introspectSchema(source);
17853
+ const { ordered, cycles } = topologicalSort(schema.tables, schema.foreignKeys);
17854
+ const tables = [];
17855
+ let totalRows = 0;
17856
+ for (const table of ordered) {
17857
+ const rowsCopied = await rowCount(source, table);
17858
+ const result = { table: tableKey(table), rowsCopied, method: "copy" };
17859
+ tables.push(result);
17860
+ totalRows += rowsCopied;
17861
+ onProgress?.(result);
17862
+ }
17863
+ const sequencesSet = schema.sequences.filter((s) => s.lastValue !== null).length;
17864
+ return {
17865
+ tables,
17866
+ sequencesSet,
17867
+ totalRows,
17868
+ warnings: [],
17869
+ deferredTables: [...cycles],
17870
+ skippedTables: [],
17871
+ // A plan writes nothing, so no table is skipped or truncated. `onExisting`
17872
+ // is echoed by `migrate` for a dry run so the preview names the policy the
17873
+ // real run would apply.
17874
+ onExisting: "error",
17875
+ truncatedTables: []
17876
+ };
17877
+ }
17878
+ async function migrate(options) {
17879
+ const { source, target, onProgress } = options;
17880
+ const onExisting = options.onExisting ?? "error";
17881
+ if (options.dryRun === true) {
17882
+ return { ...await planMigration(source, onProgress), onExisting };
17883
+ }
17884
+ const level = options.validate ?? "counts";
17885
+ const onValidationFailure = options.onValidationFailure ?? "report";
17886
+ const warnings = [];
17887
+ let reconstruction;
17888
+ if (options.reconstructSchema === true) {
17889
+ reconstruction = await reconstructSchema(source, target, {
17890
+ onUnsupported: options.onUnsupported ?? "warn"
17891
+ });
17892
+ for (const u of reconstruction.unsupported) {
17893
+ warnings.push(`Unsupported ${u.kind} not reconstructed: ${u.name}.`);
17894
+ }
17895
+ }
17896
+ const schema = await introspectSchema(source);
17897
+ const { ordered, cycles } = topologicalSort(schema.tables, schema.foreignKeys);
17898
+ const cyclicSet = new Set(cycles);
17899
+ const { skip, truncated } = await prepareTarget(target, ordered, onExisting);
17900
+ const skippedTables = [...skip];
17901
+ const tables = [];
17902
+ const deferredTables = [];
17903
+ let totalRows = 0;
17904
+ for (const table of ordered) {
17905
+ if (cyclicSet.has(tableKey(table)) || skip.has(tableKey(table))) continue;
17906
+ const result = await transferTable(source, target, table, onProgress);
17907
+ tables.push(result);
17908
+ totalRows += result.rowsCopied;
17909
+ const warning = fallbackWarning(result);
17910
+ if (warning !== null) warnings.push(warning);
17911
+ }
17912
+ if (cycles.length > 0) {
17913
+ const cyclicTables = ordered.filter(
17914
+ (t) => cyclicSet.has(tableKey(t)) && !skip.has(tableKey(t))
17915
+ );
17916
+ const cyclicResults = await transferCycle(source, target, cyclicTables, onProgress);
17917
+ for (const result of cyclicResults) {
17918
+ tables.push(result);
17919
+ totalRows += result.rowsCopied;
17920
+ deferredTables.push(result.table);
17921
+ const warning = fallbackWarning(result);
17922
+ if (warning !== null) warnings.push(warning);
17923
+ }
17924
+ }
17925
+ const sequencesSet = await applySequences(target, schema.sequences);
17926
+ let validation;
17927
+ if (level !== "off" && schema.tables.length > 0) {
17928
+ validation = await validateMigration(source, target, schema, level);
17929
+ if (!validation.ok) {
17930
+ const bad = [
17931
+ ...validation.tables.filter((t) => !t.ok).map((t) => t.table),
17932
+ ...validation.sequences.filter((s) => !s.ok).map((s) => s.sequence)
17933
+ ];
17934
+ const message2 = `Post-migration validation failed for: ${bad.join(", ")}.`;
17935
+ if (onValidationFailure === "throw") throw new ValidationError(validation, message2);
17936
+ warnings.push(message2);
17937
+ }
17938
+ }
17939
+ return {
17940
+ tables,
17941
+ sequencesSet,
17942
+ totalRows,
17943
+ warnings,
17944
+ deferredTables,
17945
+ skippedTables,
17946
+ onExisting,
17947
+ truncatedTables: truncated,
17948
+ validation,
17949
+ reconstruction
17950
+ };
17951
+ }
17952
+ async function swapIntoPlace(canonicalDir, newDir, options = {}) {
17953
+ if (!await exists(newDir)) {
17954
+ throw new Error(`New cluster directory does not exist: ${newDir}`);
17955
+ }
17956
+ const keepOld = options.keepOld ?? true;
17957
+ const timestamp2 = sanitizedTimestamp(options.timestamp);
17958
+ const oldPath = `${canonicalDir}.old-${timestamp2}`;
17959
+ const canonicalExisted = await exists(canonicalDir);
17960
+ if (canonicalExisted) {
17961
+ if (await exists(oldPath)) {
17962
+ throw new Error(`Retained-old directory already exists: ${oldPath}`);
17963
+ }
17964
+ await rename3(canonicalDir, oldPath);
17965
+ }
17966
+ try {
17967
+ await rename3(newDir, canonicalDir);
17968
+ } catch (err) {
17969
+ if (canonicalExisted) await rename3(oldPath, canonicalDir).catch(() => void 0);
17970
+ if (errorCode(err) === "EXDEV") {
17971
+ throw new Error(
17972
+ `Cannot swap: ${newDir} is on a different filesystem than ${canonicalDir}. Stage the new cluster as a sibling of the canonical location.`,
17973
+ { cause: err }
17974
+ );
17975
+ }
17976
+ throw err;
17977
+ }
17978
+ if (canonicalExisted && !keepOld) {
17979
+ await rm3(oldPath, { recursive: true, force: true });
17980
+ return { canonical: canonicalDir, previous: null };
17981
+ }
17982
+ return { canonical: canonicalDir, previous: canonicalExisted ? oldPath : null };
17983
+ }
17984
+ var __defProp2, __getOwnPropNames2, __esm2, __export2, init_fsutil, PGLITE_PACKAGE, RELEASES, UnknownMajorError, init_registry, gunzip, BLOCK, OFF, TYPE, TarError, init_tar, acquire_exports, DEFAULT_REGISTRY, FALLBACK_ENTRY, EngineFetchError, IntegrityError, init_acquire, UNRESOLVED_CODES, SYS, DETECTORS, ValidationError;
17985
+ var init_dist = __esm({
17986
+ "node_modules/pglite-migrate/dist/index.js"() {
17987
+ "use strict";
17988
+ __defProp2 = Object.defineProperty;
17989
+ __getOwnPropNames2 = Object.getOwnPropertyNames;
17990
+ __esm2 = (fn, res, err) => function __init() {
17991
+ if (err) throw err[0];
17992
+ try {
17993
+ return fn && (res = (0, fn[__getOwnPropNames2(fn)[0]])(fn = 0)), res;
17994
+ } catch (e) {
17995
+ throw err = [e], e;
17996
+ }
17997
+ };
17998
+ __export2 = (target, all) => {
17999
+ for (var name in all)
18000
+ __defProp2(target, name, { get: all[name], enumerable: true });
18001
+ };
18002
+ init_fsutil = __esm2({
18003
+ "src/fsutil.ts"() {
18004
+ "use strict";
18005
+ }
18006
+ });
18007
+ init_registry = __esm2({
18008
+ "src/engines/registry.ts"() {
18009
+ "use strict";
18010
+ PGLITE_PACKAGE = "@electric-sql/pglite";
18011
+ RELEASES = [
18012
+ {
18013
+ postgresMajor: 15,
18014
+ version: "0.1.5",
18015
+ integrity: "sha512-eymv4ONNvoPZQTvOQIi5dbpR+J5HzEv0qQH9o/y3gvNheJV/P/NFcrbsfJZYTsDKoq7DKrTiFNexsRkJKy8x9Q=="
18016
+ },
18017
+ {
18018
+ postgresMajor: 16,
18019
+ version: "0.2.17",
18020
+ integrity: "sha512-qEpKRT2oUaWDH6tjRxLHjdzMqRUGYDnGZlKrnL4dJ77JVMcP2Hpo3NYnOSPKdZdeec57B6QPprCUFg0picx5Pw=="
18021
+ },
18022
+ {
18023
+ postgresMajor: 17,
18024
+ version: "0.4.6",
18025
+ integrity: "sha512-qmlmfN8UyKCee35qkV0r/MBp+Znl8FjBz7OpoglNvww3GJpw0/DLP0o1ZymvLNmcD5DTLOQdzKPtF8Hd3mdl1w=="
18026
+ },
18027
+ {
18028
+ postgresMajor: 18,
18029
+ version: "0.5.4",
18030
+ integrity: "sha512-yYZUyyXrHU7tPlCjwZQJ6hIG9DscdCCn7Uk0mYKwC1FeHX286AbcmFveMiRBEak8e9iPupjsoVImN3yJZVed2g=="
18031
+ }
18032
+ ];
18033
+ UnknownMajorError = class extends Error {
18034
+ /** The major that could not be resolved. */
18035
+ major;
18036
+ /** The majors this build does know how to acquire. */
18037
+ knownMajors;
18038
+ constructor(major, knownMajors22) {
18039
+ super(
18040
+ `No pinned PGlite engine for PostgreSQL ${major.toString()} (this build knows ${knownMajors22.join(", ")}). Install a matching engine yourself and pass it as the source engine, e.g. npm install pglite-old@npm:${PGLITE_PACKAGE}@<version>`
18041
+ );
18042
+ this.name = "UnknownMajorError";
18043
+ this.major = major;
18044
+ this.knownMajors = knownMajors22;
18045
+ }
18046
+ };
18047
+ }
18048
+ });
18049
+ init_tar = __esm2({
18050
+ "src/engines/tar.ts"() {
18051
+ "use strict";
18052
+ gunzip = promisify(gunzipCb);
18053
+ BLOCK = 512;
18054
+ OFF = {
18055
+ name: 0,
18056
+ size: 124,
18057
+ checksum: 148,
18058
+ typeflag: 156,
18059
+ magic: 257,
18060
+ prefix: 345
18061
+ };
18062
+ TYPE = {
18063
+ file: "0",
18064
+ fileAlt: "\0",
18065
+ contiguous: "7",
18066
+ directory: "5",
18067
+ paxNext: "x",
18068
+ paxGlobal: "g",
18069
+ gnuLongName: "L"
18070
+ };
18071
+ TarError = class extends Error {
18072
+ constructor(message2) {
18073
+ super(message2);
18074
+ this.name = "TarError";
18075
+ }
18076
+ };
18077
+ }
18078
+ });
18079
+ acquire_exports = {};
18080
+ __export2(acquire_exports, {
18081
+ EngineFetchError: () => EngineFetchError,
18082
+ IntegrityError: () => IntegrityError,
18083
+ acquireEngine: () => acquireEngine,
18084
+ acquireRelease: () => acquireRelease,
18085
+ defaultCacheDir: () => defaultCacheDir,
18086
+ resolveEntry: () => resolveEntry
18087
+ });
18088
+ init_acquire = __esm2({
18089
+ "src/engines/acquire.ts"() {
18090
+ "use strict";
18091
+ init_fsutil();
18092
+ init_registry();
18093
+ init_tar();
18094
+ DEFAULT_REGISTRY = "https://registry.npmjs.org";
18095
+ FALLBACK_ENTRY = "dist/index.js";
18096
+ EngineFetchError = class extends Error {
18097
+ constructor(message2, options) {
18098
+ super(message2, options);
18099
+ this.name = "EngineFetchError";
18100
+ }
18101
+ };
18102
+ IntegrityError = class extends Error {
18103
+ constructor(expected, actual, version2) {
18104
+ super(
18105
+ `Integrity check failed for ${PGLITE_PACKAGE}@${version2}: expected ${expected}, got ${actual}. The download was discarded and nothing was written. This means the bytes served did not match the hash pinned in this build of pglite-migrate.`
18106
+ );
18107
+ this.expected = expected;
18108
+ this.actual = actual;
18109
+ this.version = version2;
18110
+ this.name = "IntegrityError";
18111
+ }
18112
+ expected;
18113
+ actual;
18114
+ version;
18115
+ };
18116
+ }
18117
+ });
18118
+ init_fsutil();
18119
+ init_registry();
18120
+ init_registry();
18121
+ init_fsutil();
18122
+ UNRESOLVED_CODES = /* @__PURE__ */ new Set([
18123
+ "ERR_MODULE_NOT_FOUND",
18124
+ "ERR_PACKAGE_PATH_NOT_EXPORTED",
18125
+ "ERR_UNSUPPORTED_DIR_IMPORT"
18126
+ ]);
18127
+ SYS = systemSchemaFilter("n.nspname");
18128
+ DETECTORS = [
18129
+ {
18130
+ // Views, matviews, partitioned tables, foreign tables — one relkind sweep.
18131
+ label: "relation",
18132
+ sql: `SELECT CASE c.relkind
18133
+ WHEN 'v' THEN 'view'
18134
+ WHEN 'm' THEN 'materialized view'
18135
+ WHEN 'p' THEN 'partitioned table'
18136
+ WHEN 'f' THEN 'foreign table'
18137
+ END AS kind,
18138
+ n.nspname || '.' || c.relname AS name
18139
+ FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace
18140
+ WHERE c.relkind IN ('v', 'm', 'p', 'f') AND ${SYS}`
18141
+ },
18142
+ {
18143
+ // Enums, domains, composites (docs/16) and function-free ranges (docs/17)
18144
+ // are reconstructed, so they are deliberately absent: reporting a type as
18145
+ // "not recreated" while recreating it would be worse than either behavior
18146
+ // alone (FR-16.3). What remains is the one genuinely un-emittable case — a
18147
+ // range whose canonical/subdiff is a function. Postgres cannot create such
18148
+ // a range in a single statement even in principle (FR-17.2/FR-17.7).
18149
+ label: "range type (depends on a canonical/subdiff function)",
18150
+ sql: `SELECT n.nspname || '.' || t.typname AS name
18151
+ FROM pg_type t
18152
+ JOIN pg_range r ON r.rngtypid = t.oid
18153
+ JOIN pg_namespace n ON n.oid = t.typnamespace
18154
+ WHERE ${SYS} AND (r.rngcanonical <> 0 OR r.rngsubdiff <> 0)`
18155
+ },
18156
+ {
18157
+ // Excludes functions Postgres creates *for* another object rather than the
18158
+ // user creating them: a range type auto-generates five constructors
18159
+ // (`intrange(int,int)`, `intmultirange(...)`, …), each carrying an INTERNAL
18160
+ // dependency on its type. Reporting those as unsupported user functions
18161
+ // would be a phantom warning on every source that declares a range.
18162
+ label: "function",
18163
+ sql: `SELECT n.nspname || '.' || p.proname AS name
18164
+ FROM pg_proc p JOIN pg_namespace n ON n.oid = p.pronamespace
18165
+ WHERE ${SYS}
18166
+ AND NOT EXISTS (SELECT 1 FROM pg_depend d
18167
+ WHERE d.objid = p.oid
18168
+ AND d.classid = 'pg_proc'::regclass
18169
+ AND d.deptype = 'i')`
18170
+ },
18171
+ {
18172
+ label: "trigger",
18173
+ sql: `SELECT n.nspname || '.' || c.relname || '.' || t.tgname AS name
18174
+ FROM pg_trigger t
18175
+ JOIN pg_class c ON c.oid = t.tgrelid
18176
+ JOIN pg_namespace n ON n.oid = c.relnamespace
18177
+ WHERE NOT t.tgisinternal AND ${SYS}`
18178
+ },
18179
+ {
18180
+ label: "policy",
18181
+ sql: `SELECT n.nspname || '.' || c.relname || '.' || pol.polname AS name
18182
+ FROM pg_policy pol
18183
+ JOIN pg_class c ON c.oid = pol.polrelid
18184
+ JOIN pg_namespace n ON n.oid = c.relnamespace
18185
+ WHERE ${SYS}`
18186
+ },
18187
+ {
18188
+ // A view's implicit _RETURN rule is the view itself, already reported above.
18189
+ label: "rule",
18190
+ sql: `SELECT n.nspname || '.' || c.relname || '.' || r.rulename AS name
18191
+ FROM pg_rewrite r
18192
+ JOIN pg_class c ON c.oid = r.ev_class
18193
+ JOIN pg_namespace n ON n.oid = c.relnamespace
18194
+ WHERE r.rulename <> '_RETURN' AND ${SYS}`
18195
+ },
18196
+ {
18197
+ label: "operator class",
18198
+ sql: `SELECT n.nspname || '.' || o.opcname AS name
18199
+ FROM pg_opclass o JOIN pg_namespace n ON n.oid = o.opcnamespace
18200
+ WHERE ${SYS}`
18201
+ },
18202
+ {
18203
+ label: "collation",
18204
+ sql: `SELECT n.nspname || '.' || col.collname AS name
18205
+ FROM pg_collation col JOIN pg_namespace n ON n.oid = col.collnamespace
18206
+ WHERE ${SYS}`
18207
+ },
18208
+ {
18209
+ // Comments on user relations and their columns. pg_description also carries
18210
+ // the built-in catalog's own docs, hence the join through pg_class.
18211
+ label: "comment",
18212
+ sql: `SELECT n.nspname || '.' || c.relname AS name
18213
+ FROM pg_description d
18214
+ JOIN pg_class c ON c.oid = d.objoid
18215
+ JOIN pg_namespace n ON n.oid = c.relnamespace
18216
+ WHERE ${SYS}
18217
+ GROUP BY n.nspname, c.relname`
18218
+ },
18219
+ {
18220
+ // A relation with no explicit grants has relacl NULL; anything else means
18221
+ // privileges were granted and would be lost.
18222
+ label: "grant",
18223
+ sql: `SELECT n.nspname || '.' || c.relname AS name
18224
+ FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace
18225
+ WHERE c.relacl IS NOT NULL AND ${SYS}`
18226
+ },
18227
+ {
18228
+ // plpgsql ships with every database; it is not something the source added.
18229
+ label: "extension",
18230
+ sql: `SELECT e.extname AS name FROM pg_extension e WHERE e.extname <> 'plpgsql'`
18231
+ }
18232
+ ];
18233
+ ValidationError = class extends Error {
18234
+ /** The validation report whose `ok` is false. */
18235
+ report;
18236
+ constructor(report, message2) {
18237
+ super(message2 ?? `Post-migration validation failed (${report.level}).`);
18238
+ this.name = "ValidationError";
18239
+ this.report = report;
18240
+ }
18241
+ };
18242
+ init_registry();
18243
+ init_fsutil();
18244
+ }
18245
+ });
18246
+
18247
+ // node_modules/pglite-migrate/dist/engines.js
18248
+ import { createHash as createHash3, randomUUID as randomUUID2 } from "crypto";
18249
+ import { mkdir as mkdir22, readFile as readFile4, rename as rename4, rm as rm4 } from "fs/promises";
18250
+ import { homedir as homedir2, tmpdir as tmpdir2 } from "os";
18251
+ import { join as join23, resolve as resolve23, sep as sep22 } from "path";
18252
+ import { stat as stat3 } from "fs/promises";
18253
+ import { mkdir as mkdir3, writeFile as writeFile2 } from "fs/promises";
18254
+ import { dirname as dirname6, join as join6, resolve as resolve5, sep as sep3 } from "path";
18255
+ import { promisify as promisify2 } from "util";
18256
+ import { gunzip as gunzipCb2 } from "zlib";
18257
+ async function exists2(path) {
18258
+ try {
18259
+ await stat3(path);
18260
+ return true;
18261
+ } catch {
18262
+ return false;
18263
+ }
18264
+ }
18265
+ function knownMajors2() {
18266
+ return RELEASES2.map((r) => r.postgresMajor);
18267
+ }
18268
+ function resolveEngine2(major) {
18269
+ const release = tryResolveEngine2(major);
18270
+ if (release === void 0) throw new UnknownMajorError2(major, knownMajors2());
18271
+ return release;
18272
+ }
18273
+ function tryResolveEngine2(major) {
18274
+ return RELEASES2.find((r) => r.postgresMajor === major);
18275
+ }
18276
+ function readOctal2(header, offset, length) {
18277
+ if ((header[offset] & 128) !== 0) {
18278
+ throw new TarError2("Unsupported base-256 numeric field in tar header");
18279
+ }
18280
+ const raw2 = header.subarray(offset, offset + length).toString("latin1").replace(/\0/g, " ").trim();
18281
+ if (raw2 === "") return 0;
18282
+ if (!/^[0-7]+$/.test(raw2)) throw new TarError2(`Malformed octal field: ${JSON.stringify(raw2)}`);
18283
+ return Number.parseInt(raw2, 8);
18284
+ }
18285
+ function readString2(header, offset, length) {
18286
+ const raw2 = header.subarray(offset, offset + length);
18287
+ const end = raw2.indexOf(0);
18288
+ return raw2.subarray(0, end === -1 ? raw2.length : end).toString("utf8");
18289
+ }
18290
+ function isZeroBlock2(block) {
18291
+ for (const byte of block) if (byte !== 0) return false;
18292
+ return true;
18293
+ }
18294
+ function verifyChecksum2(header) {
18295
+ const expected = readOctal2(header, OFF2.checksum, 8);
18296
+ let signed = 0;
18297
+ let unsigned = 0;
18298
+ for (let i = 0; i < BLOCK2; i++) {
18299
+ const inChecksumField = i >= OFF2.checksum && i < OFF2.checksum + 8;
18300
+ const byte = inChecksumField ? 32 : header[i];
18301
+ unsigned += byte;
18302
+ signed += byte > 127 ? byte - 256 : byte;
18303
+ }
18304
+ if (unsigned !== expected && signed !== expected) {
18305
+ throw new TarError2(
18306
+ `Tar header checksum mismatch (expected ${expected.toString()}, got ${unsigned.toString()})`
18307
+ );
18308
+ }
18309
+ }
18310
+ function safeEntryPath2(name, stripComponents) {
18311
+ if (name.includes("\0")) throw new TarError2(`Tar entry name contains a NUL byte`);
18312
+ if (name.includes("\\")) throw new TarError2(`Tar entry name contains a backslash: ${name}`);
18313
+ if (name.startsWith("/") || /^[A-Za-z]:/.test(name)) {
18314
+ throw new TarError2(`Absolute path in tar entry: ${name}`);
18315
+ }
18316
+ const parts = name.split("/").filter((p) => p !== "" && p !== ".");
18317
+ if (parts.includes("..")) throw new TarError2(`Path traversal in tar entry: ${name}`);
18318
+ const stripped = parts.slice(stripComponents);
18319
+ return stripped.length === 0 ? null : stripped.join("/");
18320
+ }
18321
+ function paxPath2(data) {
18322
+ const text = data.toString("utf8");
18323
+ const match = /(?:^|\n)\d+ path=([^\n]*)\n/.exec(text);
18324
+ return match?.[1];
18325
+ }
18326
+ async function extractTarGz2(gzipped, dest, stripComponents = 0) {
18327
+ const buf = await gunzip2(gzipped);
18328
+ const root = resolve5(dest);
18329
+ await mkdir3(root, { recursive: true });
18330
+ let offset = 0;
18331
+ let filesWritten = 0;
18332
+ let nextNameOverride;
18333
+ while (offset + BLOCK2 <= buf.length) {
18334
+ const header = buf.subarray(offset, offset + BLOCK2);
18335
+ if (isZeroBlock2(header)) break;
18336
+ verifyChecksum2(header);
18337
+ const size = readOctal2(header, OFF2.size, 12);
18338
+ const typeflag = String.fromCharCode(header[OFF2.typeflag]);
18339
+ const prefix = readString2(header, OFF2.prefix, 155);
18340
+ const base = readString2(header, OFF2.name, 100);
18341
+ const rawName = nextNameOverride ?? (prefix === "" ? base : `${prefix}/${base}`);
18342
+ nextNameOverride = void 0;
18343
+ const dataStart = offset + BLOCK2;
18344
+ const dataEnd = dataStart + size;
18345
+ if (dataEnd > buf.length) throw new TarError2("Tar entry extends past end of archive");
18346
+ const data = buf.subarray(dataStart, dataEnd);
18347
+ offset = dataStart + Math.ceil(size / BLOCK2) * BLOCK2;
18348
+ switch (typeflag) {
18349
+ case TYPE2.paxNext: {
18350
+ nextNameOverride = paxPath2(data);
18351
+ continue;
18352
+ }
18353
+ case TYPE2.gnuLongName: {
18354
+ nextNameOverride = data.toString("utf8").replace(/\0+$/, "");
18355
+ continue;
18356
+ }
18357
+ case TYPE2.paxGlobal:
18358
+ continue;
18359
+ // global metadata, nothing to write
18360
+ case TYPE2.file:
18361
+ case TYPE2.fileAlt:
18362
+ case TYPE2.contiguous: {
18363
+ const rel = safeEntryPath2(rawName, stripComponents);
18364
+ if (rel === null) continue;
18365
+ const full = resolve5(root, rel);
18366
+ if (full !== root && !full.startsWith(root + sep3)) {
18367
+ throw new TarError2(`Tar entry escapes extraction root: ${rawName}`);
18368
+ }
18369
+ await mkdir3(dirname6(full), { recursive: true });
18370
+ await writeFile2(full, data);
18371
+ filesWritten++;
18372
+ continue;
18373
+ }
18374
+ case TYPE2.directory: {
18375
+ const rel = safeEntryPath2(rawName, stripComponents);
18376
+ if (rel === null) continue;
18377
+ const full = resolve5(root, rel);
18378
+ if (full !== root && !full.startsWith(root + sep3)) {
18379
+ throw new TarError2(`Tar entry escapes extraction root: ${rawName}`);
18380
+ }
18381
+ await mkdir3(full, { recursive: true });
18382
+ continue;
18383
+ }
18384
+ default:
18385
+ throw new TarError2(
18386
+ `Refusing tar entry of type '${typeflag}' (${rawName}) \u2014 only regular files and directories are extracted`
18387
+ );
18388
+ }
18389
+ }
18390
+ if (filesWritten === 0) throw new TarError2(`Archive contained no files under ${join6(dest)}`);
18391
+ return filesWritten;
18392
+ }
18393
+ function defaultCacheDir2(platform = process.platform, env = process.env) {
18394
+ const parts = ["pglite-migrate", "engines"];
18395
+ if (platform === "win32") {
18396
+ const local = env.LOCALAPPDATA ?? join23(homedir2(), "AppData", "Local");
18397
+ return join23(local, ...parts);
18398
+ }
18399
+ if (platform === "darwin") {
18400
+ return join23(homedir2(), "Library", "Caches", ...parts);
18401
+ }
18402
+ const xdg = env.XDG_CACHE_HOME ?? join23(homedir2(), ".cache");
18403
+ return join23(xdg, ...parts);
18404
+ }
18405
+ function tarballUrl2(version2, registryUrl) {
18406
+ const unscoped = PGLITE_PACKAGE2.split("/").pop() ?? PGLITE_PACKAGE2;
18407
+ return `${registryUrl.replace(/\/+$/, "")}/${PGLITE_PACKAGE2}/-/${unscoped}-${version2}.tgz`;
18408
+ }
18409
+ async function download2(url2, version2) {
18410
+ let response;
18411
+ try {
18412
+ response = await fetch(url2);
18413
+ } catch (err) {
18414
+ throw new EngineFetchError2(
18415
+ `Could not reach the npm registry to download ${PGLITE_PACKAGE2}@${version2} (${url2}). If this machine is offline or behind a proxy, install the engine yourself instead: npm install pglite-old@npm:${PGLITE_PACKAGE2}@${version2}`,
18416
+ { cause: err }
18417
+ );
18418
+ }
18419
+ if (!response.ok) {
18420
+ throw new EngineFetchError2(
18421
+ `Registry returned ${response.status.toString()} ${response.statusText} for ${url2}`
18422
+ );
18423
+ }
18424
+ return Buffer.from(await response.arrayBuffer());
18425
+ }
18426
+ function verifyIntegrity2(bytes, release) {
18427
+ const actual = `sha512-${createHash3("sha512").update(bytes).digest("base64")}`;
18428
+ if (actual !== release.integrity) {
18429
+ throw new IntegrityError2(release.integrity, actual, release.version);
18430
+ }
18431
+ }
18432
+ function manifestEntryCandidates2(manifest) {
18433
+ const candidates = [];
18434
+ const dot = manifest.exports?.["."];
18435
+ if (typeof dot === "string") {
18436
+ candidates.push(dot);
18437
+ } else if (dot !== null && typeof dot === "object") {
18438
+ const imp = dot.import;
18439
+ if (typeof imp === "string") candidates.push(imp);
18440
+ }
18441
+ if (typeof manifest.module === "string") candidates.push(manifest.module);
18442
+ if (typeof manifest.main === "string") candidates.push(manifest.main);
18443
+ candidates.push(FALLBACK_ENTRY2);
18444
+ return candidates;
18445
+ }
18446
+ async function resolveEntry2(dir) {
18447
+ const root = resolve23(dir);
18448
+ let manifest = {};
18449
+ try {
18450
+ const raw2 = JSON.parse(await readFile4(join23(root, "package.json"), "utf8"));
18451
+ if (raw2 !== null && typeof raw2 === "object") manifest = raw2;
18452
+ } catch {
18453
+ }
18454
+ for (const candidate of manifestEntryCandidates2(manifest)) {
18455
+ const full = resolve23(root, candidate);
18456
+ if (full !== root && !full.startsWith(root + sep22)) continue;
18457
+ if (await exists2(full)) return full;
18458
+ }
18459
+ throw new EngineFetchError2(
18460
+ `Extracted engine at ${root} has no usable module entry point (tried the package manifest and ${FALLBACK_ENTRY2})`
18461
+ );
18462
+ }
18463
+ async function acquireEngine2(major, options = {}) {
18464
+ return acquireRelease2(resolveEngine2(major), options);
18465
+ }
18466
+ async function acquireRelease2(release, options = {}) {
18467
+ const mode = options.cache ?? "keep";
18468
+ const registryUrl = options.registryUrl ?? DEFAULT_REGISTRY2;
18469
+ if (mode === "ephemeral") {
18470
+ const parent = options.cacheDir ?? tmpdir2();
18471
+ const dir2 = join23(parent, `pglite-migrate-engine-${release.version}-${randomUUID2()}`);
18472
+ const bytes2 = await download2(tarballUrl2(release.version, registryUrl), release.version);
18473
+ verifyIntegrity2(bytes2, release);
18474
+ await extractTarGz2(bytes2, dir2, 1);
18475
+ return {
18476
+ entry: await resolveEntry2(dir2),
18477
+ dir: dir2,
18478
+ release,
18479
+ fromCache: false,
18480
+ cleanup: () => rm4(dir2, { recursive: true, force: true })
18481
+ };
18482
+ }
18483
+ const root = options.cacheDir ?? defaultCacheDir2();
18484
+ const dir = join23(root, `pglite-${release.version}`);
18485
+ const noop = () => Promise.resolve();
18486
+ if (await exists2(dir)) {
18487
+ return { entry: await resolveEntry2(dir), dir, release, fromCache: true, cleanup: noop };
18488
+ }
18489
+ const bytes = await download2(tarballUrl2(release.version, registryUrl), release.version);
18490
+ verifyIntegrity2(bytes, release);
18491
+ await mkdir22(root, { recursive: true });
18492
+ const staging = join23(root, `.staging-${release.version}-${randomUUID2()}`);
18493
+ try {
18494
+ await extractTarGz2(bytes, staging, 1);
18495
+ try {
18496
+ await rename4(staging, dir);
18497
+ } catch (err) {
18498
+ if (!await exists2(dir)) throw err;
18499
+ }
18500
+ } finally {
18501
+ await rm4(staging, { recursive: true, force: true });
18502
+ }
18503
+ return { entry: await resolveEntry2(dir), dir, release, fromCache: false, cleanup: noop };
18504
+ }
18505
+ var PGLITE_PACKAGE2, RELEASES2, UnknownMajorError2, gunzip2, BLOCK2, OFF2, TYPE2, TarError2, DEFAULT_REGISTRY2, FALLBACK_ENTRY2, EngineFetchError2, IntegrityError2;
18506
+ var init_engines = __esm({
18507
+ "node_modules/pglite-migrate/dist/engines.js"() {
18508
+ "use strict";
18509
+ PGLITE_PACKAGE2 = "@electric-sql/pglite";
18510
+ RELEASES2 = [
18511
+ {
18512
+ postgresMajor: 15,
18513
+ version: "0.1.5",
18514
+ integrity: "sha512-eymv4ONNvoPZQTvOQIi5dbpR+J5HzEv0qQH9o/y3gvNheJV/P/NFcrbsfJZYTsDKoq7DKrTiFNexsRkJKy8x9Q=="
18515
+ },
18516
+ {
18517
+ postgresMajor: 16,
18518
+ version: "0.2.17",
18519
+ integrity: "sha512-qEpKRT2oUaWDH6tjRxLHjdzMqRUGYDnGZlKrnL4dJ77JVMcP2Hpo3NYnOSPKdZdeec57B6QPprCUFg0picx5Pw=="
18520
+ },
18521
+ {
18522
+ postgresMajor: 17,
18523
+ version: "0.4.6",
18524
+ integrity: "sha512-qmlmfN8UyKCee35qkV0r/MBp+Znl8FjBz7OpoglNvww3GJpw0/DLP0o1ZymvLNmcD5DTLOQdzKPtF8Hd3mdl1w=="
18525
+ },
18526
+ {
18527
+ postgresMajor: 18,
18528
+ version: "0.5.4",
18529
+ integrity: "sha512-yYZUyyXrHU7tPlCjwZQJ6hIG9DscdCCn7Uk0mYKwC1FeHX286AbcmFveMiRBEak8e9iPupjsoVImN3yJZVed2g=="
18530
+ }
18531
+ ];
18532
+ UnknownMajorError2 = class extends Error {
18533
+ /** The major that could not be resolved. */
18534
+ major;
18535
+ /** The majors this build does know how to acquire. */
18536
+ knownMajors;
18537
+ constructor(major, knownMajors22) {
18538
+ super(
18539
+ `No pinned PGlite engine for PostgreSQL ${major.toString()} (this build knows ${knownMajors22.join(", ")}). Install a matching engine yourself and pass it as the source engine, e.g. npm install pglite-old@npm:${PGLITE_PACKAGE2}@<version>`
18540
+ );
18541
+ this.name = "UnknownMajorError";
18542
+ this.major = major;
18543
+ this.knownMajors = knownMajors22;
18544
+ }
18545
+ };
18546
+ gunzip2 = promisify2(gunzipCb2);
18547
+ BLOCK2 = 512;
18548
+ OFF2 = {
18549
+ name: 0,
18550
+ size: 124,
18551
+ checksum: 148,
18552
+ typeflag: 156,
18553
+ magic: 257,
18554
+ prefix: 345
18555
+ };
18556
+ TYPE2 = {
18557
+ file: "0",
18558
+ fileAlt: "\0",
18559
+ contiguous: "7",
18560
+ directory: "5",
18561
+ paxNext: "x",
18562
+ paxGlobal: "g",
18563
+ gnuLongName: "L"
18564
+ };
18565
+ TarError2 = class extends Error {
18566
+ constructor(message2) {
18567
+ super(message2);
18568
+ this.name = "TarError";
18569
+ }
18570
+ };
18571
+ DEFAULT_REGISTRY2 = "https://registry.npmjs.org";
18572
+ FALLBACK_ENTRY2 = "dist/index.js";
18573
+ EngineFetchError2 = class extends Error {
18574
+ constructor(message2, options) {
18575
+ super(message2, options);
18576
+ this.name = "EngineFetchError";
18577
+ }
18578
+ };
18579
+ IntegrityError2 = class extends Error {
18580
+ constructor(expected, actual, version2) {
18581
+ super(
18582
+ `Integrity check failed for ${PGLITE_PACKAGE2}@${version2}: expected ${expected}, got ${actual}. The download was discarded and nothing was written. This means the bytes served did not match the hash pinned in this build of pglite-migrate.`
18583
+ );
18584
+ this.expected = expected;
18585
+ this.actual = actual;
18586
+ this.version = version2;
18587
+ this.name = "IntegrityError";
18588
+ }
18589
+ expected;
18590
+ actual;
18591
+ version;
18592
+ };
18593
+ }
18594
+ });
18595
+
18596
+ // src/db/migrate-major.ts
16784
18597
  import { PGlite } from "@electric-sql/pglite";
16785
- import { mkdirSync as mkdirSync3, rmSync } from "fs";
16786
- import { join as join3 } from "path";
18598
+ import { existsSync as existsSync3, rmSync } from "fs";
18599
+ async function migrateMajorIfNeeded(dbPath, initSchema2) {
18600
+ if (!existsSync3(dbPath)) return { status: "not-needed" };
18601
+ let fromMajor;
18602
+ try {
18603
+ fromMajor = await readClusterVersion(dbPath);
18604
+ } catch {
18605
+ return { status: "not-needed" };
18606
+ }
18607
+ const staged = `${dbPath}.migrating-${timestamp()}`;
18608
+ let toMajor;
18609
+ try {
18610
+ toMajor = await buildTarget(staged, initSchema2);
18611
+ } catch (err) {
18612
+ discard(staged);
18613
+ return { status: "blocked", fromMajor, reason: describe3(err) };
18614
+ }
18615
+ if (fromMajor === toMajor) {
18616
+ discard(staged);
18617
+ return { status: "not-needed" };
18618
+ }
18619
+ try {
18620
+ const { rows, backupPath } = await runMigration(dbPath, staged, fromMajor);
18621
+ return { status: "migrated", fromMajor, toMajor, rows, backupPath };
18622
+ } catch (err) {
18623
+ discard(staged);
18624
+ return { status: "blocked", fromMajor, reason: describe3(err) };
18625
+ }
18626
+ }
18627
+ async function buildTarget(staged, initSchema2) {
18628
+ const target = new PGlite(staged, DB_OPTIONS);
18629
+ try {
18630
+ await target.waitReady;
18631
+ await initSchema2(target);
18632
+ } finally {
18633
+ await target.close();
18634
+ }
18635
+ return readClusterVersion(staged);
18636
+ }
18637
+ async function runMigration(dbPath, staged, fromMajor) {
18638
+ const engine = await acquireEngine2(fromMajor, { cache: "ephemeral" });
18639
+ try {
18640
+ const backupPath = await backupDataDir(dbPath, { keep: 1 });
18641
+ const source = await openDataDir(dbPath, engine.entry, { pgliteOptions: DB_OPTIONS });
18642
+ const target = new PGlite(staged, DB_OPTIONS);
18643
+ try {
18644
+ await target.waitReady;
18645
+ const schema = await introspectSchema(source);
18646
+ const obsolete = await materializeObsoleteColumns(target, schema);
18647
+ const report = await migrate({ source, target });
18648
+ const validation = await validateMigration(source, target, schema, "full");
18649
+ if (!validation.ok) {
18650
+ throw new Error(`post-migration validation failed: ${summarize(validation)}`);
18651
+ }
18652
+ await dropObsoleteColumns(target, obsolete);
18653
+ const rows = report.tables.reduce((n, t) => n + t.rowsCopied, 0);
18654
+ await source.close();
18655
+ await target.close();
18656
+ await swapIntoPlace(dbPath, staged, { keepOld: false });
18657
+ return { rows, backupPath };
18658
+ } catch (err) {
18659
+ await source.close().catch(() => void 0);
18660
+ await target.close().catch(() => void 0);
18661
+ throw err;
18662
+ }
18663
+ } finally {
18664
+ await engine.cleanup().catch(() => void 0);
18665
+ }
18666
+ }
18667
+ async function materializeObsoleteColumns(target, sourceSchema) {
18668
+ const added = [];
18669
+ for (const table of sourceSchema.tables) {
18670
+ const present = await targetColumns(target, table.schema, table.name);
18671
+ if (present === null) continue;
18672
+ for (const column of table.columns) {
18673
+ if (present.has(column.name)) continue;
18674
+ await target.exec(
18675
+ `ALTER TABLE ${quote(table.schema)}.${quote(table.name)} ADD COLUMN ${quote(column.name)} ${column.type}`
18676
+ );
18677
+ added.push({ table: `${table.schema}.${table.name}`, name: column.name });
18678
+ }
18679
+ }
18680
+ if (added.length > 0) {
18681
+ console.error(
18682
+ `Carrying over ${String(added.length)} column(s) from a removed feature so they can be verified, then discarding them: ${added.map((c) => `${c.table}.${c.name}`).join(", ")}`
18683
+ );
18684
+ }
18685
+ return added;
18686
+ }
18687
+ async function dropObsoleteColumns(target, columns) {
18688
+ for (const column of columns) {
18689
+ const [schema, table] = column.table.split(".");
18690
+ await target.exec(
18691
+ `ALTER TABLE ${quote(schema)}.${quote(table)} DROP COLUMN ${quote(column.name)}`
18692
+ );
18693
+ }
18694
+ }
18695
+ async function targetColumns(target, schema, table) {
18696
+ const { rows } = await target.query(
18697
+ `SELECT column_name FROM information_schema.columns
18698
+ WHERE table_schema = $1 AND table_name = $2`,
18699
+ [schema, table]
18700
+ );
18701
+ return rows.length === 0 ? null : new Set(rows.map((r) => r.column_name));
18702
+ }
18703
+ function quote(identifier) {
18704
+ return `"${identifier.replace(/"/g, '""')}"`;
18705
+ }
18706
+ function summarize(validation) {
18707
+ const failed = validation.tables.filter((t) => !t.ok).map((t) => t.table);
18708
+ return failed.length > 0 ? failed.join(", ") : "sequence check";
18709
+ }
18710
+ function describe3(err) {
18711
+ return err instanceof Error ? err.message : String(err);
18712
+ }
18713
+ function timestamp() {
18714
+ return (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
18715
+ }
18716
+ function discard(staged) {
18717
+ try {
18718
+ rmSync(staged, { recursive: true, force: true });
18719
+ } catch {
18720
+ }
18721
+ }
18722
+ var DB_OPTIONS;
18723
+ var init_migrate_major = __esm({
18724
+ "src/db/migrate-major.ts"() {
18725
+ "use strict";
18726
+ init_dist();
18727
+ init_engines();
18728
+ DB_OPTIONS = { database: "template1" };
18729
+ }
18730
+ });
18731
+
18732
+ // src/db/connection.ts
18733
+ import { PGlite as PGlite2 } from "@electric-sql/pglite";
18734
+ import { existsSync as existsSync4, mkdirSync as mkdirSync3, renameSync } from "fs";
18735
+ import { join as join7 } from "path";
16787
18736
  function setDataDir(dataDir) {
16788
18737
  currentDataDir = dataDir;
16789
- const dbDir = join3(dataDir, "data");
18738
+ const dbDir = join7(dataDir, "data");
16790
18739
  mkdirSync3(dbDir, { recursive: true });
16791
- currentDbPath = join3(dbDir, "reviews");
18740
+ currentDbPath = join7(dbDir, "reviews");
16792
18741
  }
16793
18742
  function getDataDir() {
16794
18743
  return currentDataDir;
16795
18744
  }
18745
+ function getDbPath() {
18746
+ return currentDbPath;
18747
+ }
16796
18748
  async function getDb() {
16797
18749
  if (db) return db;
16798
18750
  if (currentDbPath === null) throw new Error("Data directory not set. Call setDataDir() first.");
16799
18751
  try {
16800
- db = new PGlite(currentDbPath, DB_OPTIONS);
18752
+ db = new PGlite2(currentDbPath, DB_OPTIONS2);
16801
18753
  await db.waitReady;
16802
18754
  await initSchema(db);
16803
18755
  return db;
16804
18756
  } catch (err) {
16805
18757
  db = null;
16806
- const message = err instanceof Error ? err.message : String(err);
16807
- if (message.includes("Aborted") || message.includes("RuntimeError")) {
16808
- console.error("Database appears to be corrupt. Recreating...");
16809
- console.error("(Previous review data will be lost.)");
16810
- try {
16811
- rmSync(currentDbPath, { recursive: true, force: true });
16812
- } catch {
18758
+ const migration = await migrateMajorIfNeeded(currentDbPath, initSchema);
18759
+ if (migration.status === "migrated") {
18760
+ console.error(
18761
+ `Upgraded your database from PostgreSQL ${migration.fromMajor} to ${migration.toMajor} (${migration.rows} rows). A backup of the original is at: ${migration.backupPath}`
18762
+ );
18763
+ db = new PGlite2(currentDbPath, DB_OPTIONS2);
18764
+ await db.waitReady;
18765
+ await initSchema(db);
18766
+ return db;
18767
+ }
18768
+ if (migration.status === "blocked") {
18769
+ throw new Error(
18770
+ `Your database was created by PostgreSQL ${migration.fromMajor}, which this version of Glassbox cannot open directly. A one-time upgrade is needed and it could not be completed.
18771
+
18772
+ Reason: ${migration.reason}
18773
+
18774
+ Your data has not been modified. If the reason above mentions the network or the registry, reconnect and start Glassbox again \u2014 the upgrade downloads the older database engine once.`,
18775
+ { cause: err }
18776
+ );
18777
+ }
18778
+ const message2 = err instanceof Error ? err.message : String(err);
18779
+ if (message2.includes("Aborted") || message2.includes("RuntimeError")) {
18780
+ const result = quarantineDataDir(currentDbPath);
18781
+ if (!result.cleared) {
18782
+ throw err;
16813
18783
  }
16814
- db = new PGlite(currentDbPath, DB_OPTIONS);
18784
+ console.error("Database could not be opened. Starting a fresh one.");
18785
+ if (result.movedTo !== null) {
18786
+ console.error(`Your previous data has NOT been deleted \u2014 it is at: ${result.movedTo}`);
18787
+ }
18788
+ db = new PGlite2(currentDbPath, DB_OPTIONS2);
16815
18789
  await db.waitReady;
16816
18790
  await initSchema(db);
16817
18791
  return db;
@@ -16819,6 +18793,17 @@ async function getDb() {
16819
18793
  throw err;
16820
18794
  }
16821
18795
  }
18796
+ function quarantineDataDir(dbPath) {
18797
+ if (!existsSync4(dbPath)) return { cleared: true, movedTo: null };
18798
+ const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
18799
+ const quarantined = `${dbPath}.unreadable-${stamp}`;
18800
+ try {
18801
+ renameSync(dbPath, quarantined);
18802
+ return { cleared: true, movedTo: quarantined };
18803
+ } catch {
18804
+ return { cleared: false };
18805
+ }
18806
+ }
16822
18807
  async function initSchema(db2) {
16823
18808
  await db2.exec(SCHEMA_CORE_SQL);
16824
18809
  await db2.exec(SCHEMA_AI_SQL);
@@ -16848,28 +18833,29 @@ async function addColumnIfMissing(db2, table, column, definition) {
16848
18833
  await db2.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${definition}`);
16849
18834
  }
16850
18835
  }
16851
- var db, currentDbPath, currentDataDir, DB_OPTIONS;
18836
+ var db, currentDbPath, currentDataDir, DB_OPTIONS2;
16852
18837
  var init_connection = __esm({
16853
18838
  "src/db/connection.ts"() {
16854
18839
  "use strict";
16855
18840
  init_ddl();
18841
+ init_migrate_major();
16856
18842
  db = null;
16857
18843
  currentDbPath = null;
16858
18844
  currentDataDir = null;
16859
- DB_OPTIONS = { database: "template1" };
18845
+ DB_OPTIONS2 = { database: "template1" };
16860
18846
  }
16861
18847
  });
16862
18848
 
16863
18849
  // src/attachments/store.ts
16864
- import { createHash as createHash2 } from "crypto";
16865
- import { existsSync as existsSync3, mkdirSync as mkdirSync4, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
16866
- import { basename as basename2, join as join4 } from "path";
18850
+ import { createHash as createHash4 } from "crypto";
18851
+ import { existsSync as existsSync5, mkdirSync as mkdirSync4, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
18852
+ import { basename as basename3, join as join8 } from "path";
16867
18853
  function attachmentsDir() {
16868
18854
  const dataDir = getDataDir();
16869
- return dataDir === null ? null : join4(dataDir, "attachments");
18855
+ return dataDir === null ? null : join8(dataDir, "attachments");
16870
18856
  }
16871
18857
  function sanitizeFilename(name) {
16872
- const base = basename2(name.replace(/\\/g, "/"));
18858
+ const base = basename3(name.replace(/\\/g, "/"));
16873
18859
  const cleaned = base.replace(/[\x00-\x1f<>:"|?*/\\]/g, "").trim();
16874
18860
  return cleaned === "" ? "attachment" : cleaned.slice(0, 200);
16875
18861
  }
@@ -16877,14 +18863,14 @@ function writeAttachmentFile(id, originalFilename, bytes) {
16877
18863
  const dir = attachmentsDir();
16878
18864
  if (dir === null) throw new Error("No data directory for attachments");
16879
18865
  mkdirSync4(dir, { recursive: true });
16880
- const storedPath = join4(dir, `${id}-${sanitizeFilename(originalFilename)}`);
18866
+ const storedPath = join8(dir, `${id}-${sanitizeFilename(originalFilename)}`);
16881
18867
  writeFileSync2(storedPath, bytes);
16882
- const sha256 = createHash2("sha256").update(bytes).digest("hex");
18868
+ const sha256 = createHash4("sha256").update(bytes).digest("hex");
16883
18869
  return { storedPath, size: bytes.length, sha256 };
16884
18870
  }
16885
18871
  function deleteAttachmentFile(storedPath) {
16886
18872
  try {
16887
- if (existsSync3(storedPath)) rmSync2(storedPath, { force: true });
18873
+ if (existsSync5(storedPath)) rmSync2(storedPath, { force: true });
16888
18874
  } catch {
16889
18875
  }
16890
18876
  }
@@ -17384,17 +19370,17 @@ var init_queries = __esm({
17384
19370
  });
17385
19371
 
17386
19372
  // src/global-config.ts
17387
- import { chmodSync, existsSync as existsSync4, mkdirSync as mkdirSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
17388
- import { homedir } from "os";
17389
- import { join as join5 } from "path";
19373
+ import { chmodSync, existsSync as existsSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
19374
+ import { homedir as homedir3 } from "os";
19375
+ import { join as join9 } from "path";
17390
19376
  function resolveGlobalConfigDir() {
17391
19377
  const override = process.env.GLASSBOX_CONFIG_DIR;
17392
19378
  if (override !== void 0 && override.trim() !== "") return override;
17393
- return join5(homedir(), ".glassbox");
19379
+ return join9(homedir3(), ".glassbox");
17394
19380
  }
17395
19381
  function readGlobalConfig() {
17396
19382
  try {
17397
- if (existsSync4(GLOBAL_CONFIG_PATH)) {
19383
+ if (existsSync6(GLOBAL_CONFIG_PATH)) {
17398
19384
  const raw2 = JSON.parse(readFileSync4(GLOBAL_CONFIG_PATH, "utf-8"));
17399
19385
  const parsed = GlobalConfigSchema.safeParse(raw2);
17400
19386
  if (parsed.success) return parsed.data;
@@ -17423,7 +19409,7 @@ var init_global_config = __esm({
17423
19409
  init_zod();
17424
19410
  GlobalConfigSchema = external_exports.record(external_exports.string(), external_exports.unknown());
17425
19411
  GLOBAL_CONFIG_DIR = resolveGlobalConfigDir();
17426
- GLOBAL_CONFIG_PATH = join5(GLOBAL_CONFIG_DIR, "config.json");
19412
+ GLOBAL_CONFIG_PATH = join9(GLOBAL_CONFIG_DIR, "config.json");
17427
19413
  }
17428
19414
  });
17429
19415
 
@@ -17562,10 +19548,10 @@ __export(image_blobs_exports, {
17562
19548
  readImageBlob: () => readImageBlob,
17563
19549
  writeImageBlob: () => writeImageBlob
17564
19550
  });
17565
- import { existsSync as existsSync5, mkdirSync as mkdirSync6, readFileSync as readFileSync5, rmSync as rmSync3, writeFileSync as writeFileSync4 } from "fs";
17566
- import { join as join6 } from "path";
19551
+ import { existsSync as existsSync7, mkdirSync as mkdirSync6, readFileSync as readFileSync5, rmSync as rmSync3, writeFileSync as writeFileSync4 } from "fs";
19552
+ import { join as join10 } from "path";
17567
19553
  function blobDir(dataDir) {
17568
- return join6(dataDir, "image-blobs");
19554
+ return join10(dataDir, "image-blobs");
17569
19555
  }
17570
19556
  function blobName(fileId, side) {
17571
19557
  return `${fileId.replace(/[^a-z0-9]/gi, "")}-${side}`;
@@ -17574,11 +19560,11 @@ function writeImageBlob(dataDir, fileId, side, bytes) {
17574
19560
  if (bytes.length === 0) return;
17575
19561
  const dir = blobDir(dataDir);
17576
19562
  mkdirSync6(dir, { recursive: true });
17577
- writeFileSync4(join6(dir, blobName(fileId, side)), bytes);
19563
+ writeFileSync4(join10(dir, blobName(fileId, side)), bytes);
17578
19564
  }
17579
19565
  function readImageBlob(dataDir, fileId, side) {
17580
- const path = join6(blobDir(dataDir), blobName(fileId, side));
17581
- if (!existsSync5(path)) return null;
19566
+ const path = join10(blobDir(dataDir), blobName(fileId, side));
19567
+ if (!existsSync7(path)) return null;
17582
19568
  try {
17583
19569
  return readFileSync5(path);
17584
19570
  } catch {
@@ -17680,15 +19666,15 @@ var init_project_settings = __esm({
17680
19666
  });
17681
19667
 
17682
19668
  // src/project-settings-store.ts
17683
- import { existsSync as existsSync10, mkdirSync as mkdirSync8, readFileSync as readFileSync10, writeFileSync as writeFileSync9 } from "fs";
17684
- import { join as join11 } from "path";
19669
+ import { existsSync as existsSync12, mkdirSync as mkdirSync8, readFileSync as readFileSync10, writeFileSync as writeFileSync9 } from "fs";
19670
+ import { join as join15 } from "path";
17685
19671
  function settingsPath(repoRoot) {
17686
- return join11(repoRoot, ".glassbox", "settings.json");
19672
+ return join15(repoRoot, ".glassbox", "settings.json");
17687
19673
  }
17688
19674
  function readProjectSettings(repoRoot) {
17689
19675
  try {
17690
19676
  const path = settingsPath(repoRoot);
17691
- if (existsSync10(path)) {
19677
+ if (existsSync12(path)) {
17692
19678
  const raw2 = JSON.parse(readFileSync10(path, "utf-8"));
17693
19679
  const parsed = ProjectSettingsSchema.safeParse(raw2);
17694
19680
  if (parsed.success) return parsed.data;
@@ -17701,9 +19687,9 @@ function updateProjectSettings2(repoRoot, mutator) {
17701
19687
  const current = readProjectSettings(repoRoot);
17702
19688
  const result = mutator(current);
17703
19689
  const next = result === void 0 ? current : result;
17704
- const dir = join11(repoRoot, ".glassbox");
19690
+ const dir = join15(repoRoot, ".glassbox");
17705
19691
  mkdirSync8(dir, { recursive: true });
17706
- writeFileSync9(join11(dir, "settings.json"), JSON.stringify(next, null, 2), "utf-8");
19692
+ writeFileSync9(join15(dir, "settings.json"), JSON.stringify(next, null, 2), "utf-8");
17707
19693
  }
17708
19694
  var init_project_settings_store = __esm({
17709
19695
  "src/project-settings-store.ts"() {
@@ -17922,7 +19908,7 @@ function pickBest(handlers, input) {
17922
19908
  return best;
17923
19909
  }
17924
19910
  var ContentPluginRegistry;
17925
- var init_registry = __esm({
19911
+ var init_registry2 = __esm({
17926
19912
  "src/plugins/registry.ts"() {
17927
19913
  "use strict";
17928
19914
  ContentPluginRegistry = class {
@@ -18057,9 +20043,9 @@ var init_settings = __esm({
18057
20043
  });
18058
20044
 
18059
20045
  // src/plugins/loader.ts
18060
- import { existsSync as existsSync11, readdirSync as readdirSync2, readFileSync as readFileSync11, statSync as statSync3 } from "fs";
18061
- import { join as join12 } from "path";
18062
- import { pathToFileURL } from "url";
20046
+ import { existsSync as existsSync13, readdirSync as readdirSync2, readFileSync as readFileSync11, statSync as statSync3 } from "fs";
20047
+ import { join as join16 } from "path";
20048
+ import { pathToFileURL as pathToFileURL2 } from "url";
18063
20049
  function getConfigLabelOverride(pluginId, labelId) {
18064
20050
  return configLabelOverrides.get(`${pluginId}:${labelId}`);
18065
20051
  }
@@ -18079,10 +20065,10 @@ function clearAllPluginUIElements() {
18079
20065
  pluginUIElements.clear();
18080
20066
  }
18081
20067
  function pluginsDir() {
18082
- return join12(GLOBAL_CONFIG_DIR, "plugins");
20068
+ return join16(GLOBAL_CONFIG_DIR, "plugins");
18083
20069
  }
18084
20070
  function discoverPluginDirs(root = pluginsDir()) {
18085
- if (!existsSync11(root)) return [];
20071
+ if (!existsSync13(root)) return [];
18086
20072
  let entries;
18087
20073
  try {
18088
20074
  entries = readdirSync2(root);
@@ -18092,7 +20078,7 @@ function discoverPluginDirs(root = pluginsDir()) {
18092
20078
  const out = [];
18093
20079
  for (const name of entries) {
18094
20080
  if (name.startsWith(".")) continue;
18095
- const dir = join12(root, name);
20081
+ const dir = join16(root, name);
18096
20082
  try {
18097
20083
  if (statSync3(dir).isDirectory()) out.push(dir);
18098
20084
  } catch {
@@ -18101,16 +20087,16 @@ function discoverPluginDirs(root = pluginsDir()) {
18101
20087
  return out.sort();
18102
20088
  }
18103
20089
  function readManifest(dir) {
18104
- const manifestPath = join12(dir, "manifest.json");
18105
- if (existsSync11(manifestPath)) {
20090
+ const manifestPath = join16(dir, "manifest.json");
20091
+ if (existsSync13(manifestPath)) {
18106
20092
  try {
18107
20093
  return parseManifest(JSON.parse(readFileSync11(manifestPath, "utf-8")));
18108
20094
  } catch {
18109
20095
  return null;
18110
20096
  }
18111
20097
  }
18112
- const pkgPath = join12(dir, "package.json");
18113
- if (existsSync11(pkgPath)) {
20098
+ const pkgPath = join16(dir, "package.json");
20099
+ if (existsSync13(pkgPath)) {
18114
20100
  try {
18115
20101
  const pkg = JSON.parse(readFileSync11(pkgPath, "utf-8"));
18116
20102
  const gb = pkg.glassbox;
@@ -18126,8 +20112,8 @@ function readManifest(dir) {
18126
20112
  function makeContext(manifest, repoRoot) {
18127
20113
  const id = manifest.id;
18128
20114
  return {
18129
- log: (level, message) => {
18130
- const line = `[plugin:${id}] ${message}`;
20115
+ log: (level, message2) => {
20116
+ const line = `[plugin:${id}] ${message2}`;
18131
20117
  if (level === "error") console.error(line);
18132
20118
  else if (level === "warn") console.warn(line);
18133
20119
  else console.log(line);
@@ -18155,9 +20141,9 @@ async function loadPluginDir(dir, registry3, isEnabled = ALWAYS_ENABLED, repoRoo
18155
20141
  return { id: manifest.id, dir, manifest, status: "disabled", disabledScope: enablement.scope };
18156
20142
  }
18157
20143
  try {
18158
- const entry = join12(dir, manifest.entry ?? "index.js");
18159
- if (!existsSync11(entry)) throw new Error(`entry not found: ${manifest.entry ?? "index.js"}`);
18160
- const mod = await import(pathToFileURL(entry).href);
20144
+ const entry = join16(dir, manifest.entry ?? "index.js");
20145
+ if (!existsSync13(entry)) throw new Error(`entry not found: ${manifest.entry ?? "index.js"}`);
20146
+ const mod = await import(pathToFileURL2(entry).href);
18161
20147
  const plugin = mod.default ?? (typeof mod.activate === "function" ? { activate: mod.activate, onAction: mod.onAction } : void 0);
18162
20148
  if (plugin === void 0 || typeof plugin.activate !== "function") {
18163
20149
  throw new Error("plugin exports no activate()");
@@ -18190,7 +20176,7 @@ var init_loader = __esm({
18190
20176
  init_feature_flags();
18191
20177
  init_global_config();
18192
20178
  init_manifest2();
18193
- init_registry();
20179
+ init_registry2();
18194
20180
  init_settings();
18195
20181
  configLabelOverrides = /* @__PURE__ */ new Map();
18196
20182
  pluginUIElements = /* @__PURE__ */ new Map();
@@ -18199,22 +20185,22 @@ var init_loader = __esm({
18199
20185
  });
18200
20186
 
18201
20187
  // src/plugins/install.ts
18202
- import { createHash as createHash3 } from "crypto";
18203
- import { cpSync, existsSync as existsSync12, mkdirSync as mkdirSync9, readdirSync as readdirSync3, readFileSync as readFileSync12, rmSync as rmSync6, statSync as statSync4, symlinkSync, writeFileSync as writeFileSync10 } from "fs";
18204
- import { dirname as dirname8, join as join13 } from "path";
20188
+ import { createHash as createHash5 } from "crypto";
20189
+ import { cpSync, existsSync as existsSync14, mkdirSync as mkdirSync9, readdirSync as readdirSync3, readFileSync as readFileSync12, rmSync as rmSync6, statSync as statSync4, symlinkSync, writeFileSync as writeFileSync10 } from "fs";
20190
+ import { dirname as dirname10, join as join17 } from "path";
18205
20191
  import { fileURLToPath as fileURLToPath3 } from "url";
18206
20192
  function dismissedPathFor(userDir) {
18207
- return join13(dirname8(userDir), DISMISSED_FILE);
20193
+ return join17(dirname10(userDir), DISMISSED_FILE);
18208
20194
  }
18209
20195
  function bundledPluginsDir() {
18210
20196
  const override = process.env.GLASSBOX_BUNDLED_PLUGINS_DIR;
18211
20197
  if (override !== void 0 && override.trim() !== "") return override;
18212
- const sibling = join13(dirname8(fileURLToPath3(import.meta.url)), "plugins");
18213
- if (existsSync12(sibling)) return sibling;
18214
- return join13(process.cwd(), "dist", "plugins");
20198
+ const sibling = join17(dirname10(fileURLToPath3(import.meta.url)), "plugins");
20199
+ if (existsSync14(sibling)) return sibling;
20200
+ return join17(process.cwd(), "dist", "plugins");
18215
20201
  }
18216
20202
  function discoverBundledPlugins(bundledDir = bundledPluginsDir()) {
18217
- if (!existsSync12(bundledDir)) return [];
20203
+ if (!existsSync14(bundledDir)) return [];
18218
20204
  let entries;
18219
20205
  try {
18220
20206
  entries = readdirSync3(bundledDir);
@@ -18223,7 +20209,7 @@ function discoverBundledPlugins(bundledDir = bundledPluginsDir()) {
18223
20209
  }
18224
20210
  const out = [];
18225
20211
  for (const name of entries.sort()) {
18226
- const dir = join13(bundledDir, name);
20212
+ const dir = join17(bundledDir, name);
18227
20213
  try {
18228
20214
  if (!statSync4(dir).isDirectory()) continue;
18229
20215
  const manifest = readManifest(dir);
@@ -18243,14 +20229,14 @@ function readDismissed(userDir = pluginsDir()) {
18243
20229
  }
18244
20230
  function writeDismissed(ids, userDir) {
18245
20231
  const path = dismissedPathFor(userDir);
18246
- mkdirSync9(dirname8(path), { recursive: true });
20232
+ mkdirSync9(dirname10(path), { recursive: true });
18247
20233
  writeFileSync10(path, JSON.stringify([...new Set(ids)], null, 2), "utf-8");
18248
20234
  }
18249
20235
  function hashPluginDir(dir) {
18250
- const h = createHash3("sha1");
20236
+ const h = createHash5("sha1");
18251
20237
  const walk = (d, rel) => {
18252
20238
  for (const name of readdirSync3(d).sort()) {
18253
- const abs = join13(d, name);
20239
+ const abs = join17(d, name);
18254
20240
  const r = rel === "" ? name : `${rel}/${name}`;
18255
20241
  if (statSync4(abs).isDirectory()) walk(abs, r);
18256
20242
  else {
@@ -18263,7 +20249,7 @@ function hashPluginDir(dir) {
18263
20249
  return h.digest("hex");
18264
20250
  }
18265
20251
  function shouldInstall(src, dest, bundledVersion) {
18266
- if (!existsSync12(dest)) return true;
20252
+ if (!existsSync14(dest)) return true;
18267
20253
  const destManifest = readManifest(dest);
18268
20254
  const destVersion = destManifest?.version ?? "0";
18269
20255
  const cmp = compareVersions(bundledVersion, destVersion);
@@ -18278,7 +20264,7 @@ function shouldInstall(src, dest, bundledVersion) {
18278
20264
  function installBundledPlugins(opts) {
18279
20265
  const bundledDir = opts?.bundledDir ?? bundledPluginsDir();
18280
20266
  const userDir = opts?.userDir ?? pluginsDir();
18281
- if (!existsSync12(bundledDir)) return;
20267
+ if (!existsSync14(bundledDir)) return;
18282
20268
  const dismissed = new Set(readDismissed(userDir));
18283
20269
  let entries;
18284
20270
  try {
@@ -18287,13 +20273,13 @@ function installBundledPlugins(opts) {
18287
20273
  return;
18288
20274
  }
18289
20275
  for (const name of entries) {
18290
- const src = join13(bundledDir, name);
20276
+ const src = join17(bundledDir, name);
18291
20277
  try {
18292
20278
  if (!statSync4(src).isDirectory()) continue;
18293
20279
  const manifest = readManifest(src);
18294
20280
  if (manifest === null || dismissed.has(manifest.id)) continue;
18295
20281
  if (manifest.autoInstall === false) continue;
18296
- const dest = join13(userDir, manifest.id);
20282
+ const dest = join17(userDir, manifest.id);
18297
20283
  if (!shouldInstall(src, dest, manifest.version)) continue;
18298
20284
  mkdirSync9(userDir, { recursive: true });
18299
20285
  rmSync6(dest, { recursive: true, force: true });
@@ -18307,7 +20293,7 @@ function installPluginFromDisk(sourceDir, opts) {
18307
20293
  if (manifest === null) throw new Error("The selected folder is not a Glassbox plugin (no manifest.json).");
18308
20294
  const userDir = opts?.userDir ?? pluginsDir();
18309
20295
  mkdirSync9(userDir, { recursive: true });
18310
- const dest = join13(userDir, manifest.id);
20296
+ const dest = join17(userDir, manifest.id);
18311
20297
  rmSync6(dest, { recursive: true, force: true });
18312
20298
  try {
18313
20299
  symlinkSync(sourceDir, dest, "dir");
@@ -18322,7 +20308,7 @@ function undismissPlugin(id, userDir = pluginsDir()) {
18322
20308
  }
18323
20309
  function uninstallPlugin(id, opts) {
18324
20310
  const userDir = opts?.userDir ?? pluginsDir();
18325
- rmSync6(join13(userDir, id), { recursive: true, force: true });
20311
+ rmSync6(join17(userDir, id), { recursive: true, force: true });
18326
20312
  writeDismissed([...readDismissed(userDir), id], userDir);
18327
20313
  }
18328
20314
  var DISMISSED_FILE;
@@ -18633,7 +20619,7 @@ var init_plugins = __esm({
18633
20619
  init_enablement();
18634
20620
  init_install();
18635
20621
  init_loader();
18636
- init_registry();
20622
+ init_registry2();
18637
20623
  init_settings();
18638
20624
  init_svgSize();
18639
20625
  init_hooks();
@@ -18737,17 +20723,17 @@ __export(difftool_discovery_exports, {
18737
20723
  tryAcquireStartingLock: () => tryAcquireStartingLock,
18738
20724
  writeDiscovery: () => writeDiscovery
18739
20725
  });
18740
- import { existsSync as existsSync19, mkdirSync as mkdirSync16, readFileSync as readFileSync23, rmSync as rmSync7, statSync as statSync7, writeFileSync as writeFileSync16 } from "fs";
18741
- import { homedir as homedir3 } from "os";
18742
- import { join as join24 } from "path";
20726
+ import { existsSync as existsSync21, mkdirSync as mkdirSync16, readFileSync as readFileSync23, rmSync as rmSync8, statSync as statSync8, writeFileSync as writeFileSync16 } from "fs";
20727
+ import { homedir as homedir5 } from "os";
20728
+ import { join as join31 } from "path";
18743
20729
  function difftoolHome() {
18744
- return join24(homedir3(), ".glassbox");
20730
+ return join31(homedir5(), ".glassbox");
18745
20731
  }
18746
20732
  function discoveryPath(home = difftoolHome()) {
18747
- return join24(home, "difftool.lock");
20733
+ return join31(home, "difftool.lock");
18748
20734
  }
18749
20735
  function startingLockPath(home = difftoolHome()) {
18750
- return join24(home, "difftool-starting.lock");
20736
+ return join31(home, "difftool-starting.lock");
18751
20737
  }
18752
20738
  function parseDiscovery(raw2) {
18753
20739
  let parsed;
@@ -18761,7 +20747,7 @@ function parseDiscovery(raw2) {
18761
20747
  }
18762
20748
  function readDiscovery(home = difftoolHome()) {
18763
20749
  const path = discoveryPath(home);
18764
- if (!existsSync19(path)) return null;
20750
+ if (!existsSync21(path)) return null;
18765
20751
  try {
18766
20752
  return parseDiscovery(readFileSync23(path, "utf-8"));
18767
20753
  } catch {
@@ -18774,7 +20760,7 @@ function writeDiscovery(port, home = difftoolHome()) {
18774
20760
  }
18775
20761
  function clearDiscovery(home = difftoolHome()) {
18776
20762
  try {
18777
- rmSync7(discoveryPath(home), { force: true });
20763
+ rmSync8(discoveryPath(home), { force: true });
18778
20764
  } catch {
18779
20765
  }
18780
20766
  }
@@ -18786,9 +20772,9 @@ function tryAcquireStartingLock(home = difftoolHome()) {
18786
20772
  return true;
18787
20773
  } catch {
18788
20774
  try {
18789
- const ageMs = Date.now() - statSync7(path).mtimeMs;
20775
+ const ageMs = Date.now() - statSync8(path).mtimeMs;
18790
20776
  if (ageMs > STARTING_LOCK_STALE_MS) {
18791
- rmSync7(path, { force: true });
20777
+ rmSync8(path, { force: true });
18792
20778
  writeFileSync16(path, String(process.pid), { flag: "wx" });
18793
20779
  return true;
18794
20780
  }
@@ -18799,7 +20785,7 @@ function tryAcquireStartingLock(home = difftoolHome()) {
18799
20785
  }
18800
20786
  function releaseStartingLock(home = difftoolHome()) {
18801
20787
  try {
18802
- rmSync7(startingLockPath(home), { force: true });
20788
+ rmSync8(startingLockPath(home), { force: true });
18803
20789
  } catch {
18804
20790
  }
18805
20791
  }
@@ -20692,10 +22678,10 @@ var require_decoder = __commonJS({
20692
22678
  if (frames.length != 1)
20693
22679
  throw new Error("only single frame JPEGs supported");
20694
22680
  for (var i = 0; i < frames.length; i++) {
20695
- var cp = frames[i].components;
20696
- for (var j in cp) {
20697
- cp[j].quantizationTable = quantizationTables[cp[j].quantizationIdx];
20698
- delete cp[j].quantizationIdx;
22681
+ var cp2 = frames[i].components;
22682
+ for (var j in cp2) {
22683
+ cp2[j].quantizationTable = quantizationTables[cp2[j].quantizationIdx];
22684
+ delete cp2[j].quantizationIdx;
20699
22685
  }
20700
22686
  }
20701
22687
  this.width = frame.samplesPerLine;
@@ -23332,9 +25318,9 @@ var init_perceptual_diff = __esm({
23332
25318
  });
23333
25319
 
23334
25320
  // src/cli.ts
23335
- import { existsSync as existsSync20, mkdirSync as mkdirSync17, realpathSync, statSync as statSync8 } from "fs";
23336
- import { tmpdir as tmpdir2 } from "os";
23337
- import { basename as basename5, join as join25, resolve as resolve12 } from "path";
25321
+ import { existsSync as existsSync22, mkdirSync as mkdirSync17, realpathSync, statSync as statSync9 } from "fs";
25322
+ import { tmpdir as tmpdir4 } from "os";
25323
+ import { basename as basename7, join as join32, resolve as resolve14 } from "path";
23338
25324
 
23339
25325
  // src/cli-subcommands.ts
23340
25326
  import { resolve as resolve3 } from "path";
@@ -24455,9 +26441,9 @@ async function setupReviewNoteReply(fileIdMap) {
24455
26441
 
24456
26442
  // src/git/diff.ts
24457
26443
  init_zod();
24458
- import { existsSync as existsSync6, mkdirSync as mkdirSync7, mkdtempSync, readFileSync as readFileSync6, rmSync as rmSync4, statSync as statSync2, writeFileSync as writeFileSync5 } from "fs";
24459
- import { tmpdir } from "os";
24460
- import { basename as basename3, dirname as dirname5, join as join7, resolve as resolve4 } from "path";
26444
+ import { existsSync as existsSync8, mkdirSync as mkdirSync7, mkdtempSync, readFileSync as readFileSync6, rmSync as rmSync4, statSync as statSync2, writeFileSync as writeFileSync5 } from "fs";
26445
+ import { tmpdir as tmpdir3 } from "os";
26446
+ import { basename as basename4, dirname as dirname7, join as join11, resolve as resolve6 } from "path";
24461
26447
 
24462
26448
  // src/git/parseDiff.ts
24463
26449
  init_lfs();
@@ -24688,9 +26674,9 @@ function getDiffArgs(mode) {
24688
26674
  function directComparisonRoots(mode) {
24689
26675
  const rootOf = (p) => {
24690
26676
  try {
24691
- return statSync2(p).isDirectory() ? p : dirname5(p);
26677
+ return statSync2(p).isDirectory() ? p : dirname7(p);
24692
26678
  } catch {
24693
- return dirname5(p);
26679
+ return dirname7(p);
24694
26680
  }
24695
26681
  };
24696
26682
  return { rootA: rootOf(mode.pathA), rootB: rootOf(mode.pathB) };
@@ -24718,14 +26704,14 @@ function getDirectComparisonFiles(mode, cwd) {
24718
26704
  function diffRawContent(displayPath, oldContent, newContent) {
24719
26705
  const rel = displayPath.replace(/\\/g, "/").replace(/^\/+/, "").replace(/(^|\/)\.\.(?=\/|$)/g, "$1_");
24720
26706
  const safeRel = rel === "" ? "file" : rel;
24721
- const work = mkdtempSync(join7(tmpdir(), "glassbox-difftool-append-"));
24722
- const rootA = join7(work, "a");
24723
- const rootB = join7(work, "b");
24724
- const oldAbs = join7(rootA, safeRel);
24725
- const newAbs = join7(rootB, safeRel);
26707
+ const work = mkdtempSync(join11(tmpdir3(), "glassbox-difftool-append-"));
26708
+ const rootA = join11(work, "a");
26709
+ const rootB = join11(work, "b");
26710
+ const oldAbs = join11(rootA, safeRel);
26711
+ const newAbs = join11(rootB, safeRel);
24726
26712
  try {
24727
- mkdirSync7(dirname5(oldAbs), { recursive: true });
24728
- mkdirSync7(dirname5(newAbs), { recursive: true });
26713
+ mkdirSync7(dirname7(oldAbs), { recursive: true });
26714
+ mkdirSync7(dirname7(newAbs), { recursive: true });
24729
26715
  writeFileSync5(oldAbs, oldContent);
24730
26716
  writeFileSync5(newAbs, newContent);
24731
26717
  const rawDiff = gitOrEmpty(["diff", "--no-index", "-U3", toGitArg(oldAbs), toGitArg(newAbs)], work);
@@ -24789,7 +26775,7 @@ function getAllFiles(repoRoot) {
24789
26775
  function createNewFileDiff(filePath, repoRoot) {
24790
26776
  let content;
24791
26777
  try {
24792
- const buf = readFileSync6(resolve4(repoRoot, filePath));
26778
+ const buf = readFileSync6(resolve6(repoRoot, filePath));
24793
26779
  const checkLen = Math.min(buf.length, 8192);
24794
26780
  for (let i = 0; i < checkLen; i++) {
24795
26781
  if (buf[i] === 0) {
@@ -24825,7 +26811,7 @@ function getFileContent(filePath, ref, cwd) {
24825
26811
  const repoRoot = getRepoRoot(cwd);
24826
26812
  try {
24827
26813
  if (ref === "working") {
24828
- return readFileSync6(resolve4(repoRoot, filePath), "utf-8");
26814
+ return readFileSync6(resolve6(repoRoot, filePath), "utf-8");
24829
26815
  }
24830
26816
  return git(["show", `${ref}:${filePath}`], repoRoot);
24831
26817
  } catch {
@@ -24835,7 +26821,7 @@ function getFileContent(filePath, ref, cwd) {
24835
26821
  function getModeFileContent(mode, filePath, side, cwd) {
24836
26822
  if (mode.type === "diff") {
24837
26823
  const { rootA, rootB } = directComparisonRoots(mode);
24838
- const abs = join7(side === "old" ? rootA : rootB, filePath);
26824
+ const abs = join11(side === "old" ? rootA : rootB, filePath);
24839
26825
  try {
24840
26826
  return readFileSync6(abs, "utf-8");
24841
26827
  } catch {
@@ -24882,9 +26868,9 @@ function getSingleFileDiff(mode, filePath, repoRoot, extraFlags = "") {
24882
26868
  if (mode.type === "ground-truth") return null;
24883
26869
  if (mode.type === "diff") {
24884
26870
  const { rootA, rootB } = directComparisonRoots(mode);
24885
- const oldAbs = join7(rootA, filePath);
24886
- const newAbs = join7(rootB, filePath);
24887
- if (!existsSync6(oldAbs) || !existsSync6(newAbs)) return null;
26871
+ const oldAbs = join11(rootA, filePath);
26872
+ const newAbs = join11(rootB, filePath);
26873
+ if (!existsSync8(oldAbs) || !existsSync8(newAbs)) return null;
24888
26874
  const args2 = ["diff", "--no-index", "-U3"];
24889
26875
  if (extraFlags) args2.push(...extraFlags.split(" ").filter(Boolean));
24890
26876
  args2.push(toGitArg(oldAbs), toGitArg(newAbs));
@@ -24937,9 +26923,9 @@ function getModeArgs(mode) {
24937
26923
  // A short, human-readable label for the sidebar/history. Full path
24938
26924
  // disambiguation lives in the mode string, so basenames are fine here.
24939
26925
  case "diff":
24940
- return `${basename3(mode.pathA)} \u2194 ${basename3(mode.pathB)}`;
26926
+ return `${basename4(mode.pathA)} \u2194 ${basename4(mode.pathB)}`;
24941
26927
  case "ground-truth":
24942
- return basename3(mode.manifestPath);
26928
+ return basename4(mode.manifestPath);
24943
26929
  case "uncommitted":
24944
26930
  case "staged":
24945
26931
  case "unstaged":
@@ -24949,8 +26935,8 @@ function getModeArgs(mode) {
24949
26935
  }
24950
26936
 
24951
26937
  // src/git/gitignore.ts
24952
- import { existsSync as existsSync7, readFileSync as readFileSync7, writeFileSync as writeFileSync6 } from "fs";
24953
- import { basename as basename4, dirname as dirname6, join as join8 } from "path";
26938
+ import { existsSync as existsSync9, readFileSync as readFileSync7, writeFileSync as writeFileSync6 } from "fs";
26939
+ import { basename as basename5, dirname as dirname8, join as join12 } from "path";
24954
26940
  var GLASSBOX_GITIGNORE_LINES = ["/.glassbox/*", "!/.glassbox/settings.json"];
24955
26941
  function isGlassboxPattern(token) {
24956
26942
  const core = token.replace(/^!/, "").replace(/^\//, "").trim();
@@ -25000,11 +26986,11 @@ function computeGitignore(existing) {
25000
26986
  return { changed: true, content };
25001
26987
  }
25002
26988
  function ensureGlassboxGitignored(glassboxDir) {
25003
- if (basename4(glassboxDir) !== ".glassbox") return { changed: false };
25004
- const targetDir = dirname6(glassboxDir);
26989
+ if (basename5(glassboxDir) !== ".glassbox") return { changed: false };
26990
+ const targetDir = dirname8(glassboxDir);
25005
26991
  if (!isGitRepo(targetDir)) return { changed: false };
25006
- const gitignorePath = join8(targetDir, ".gitignore");
25007
- const existing = existsSync7(gitignorePath) ? readFileSync7(gitignorePath, "utf-8") : null;
26992
+ const gitignorePath = join12(targetDir, ".gitignore");
26993
+ const existing = existsSync9(gitignorePath) ? readFileSync7(gitignorePath, "utf-8") : null;
25008
26994
  const { changed, content } = computeGitignore(existing);
25009
26995
  if (changed) writeFileSync6(gitignorePath, content, "utf-8");
25010
26996
  return { changed };
@@ -25012,13 +26998,13 @@ function ensureGlassboxGitignored(glassboxDir) {
25012
26998
 
25013
26999
  // src/lock.ts
25014
27000
  init_zod();
25015
- import { existsSync as existsSync8, readFileSync as readFileSync8, rmSync as rmSync5, writeFileSync as writeFileSync7 } from "fs";
25016
- import { join as join9 } from "path";
27001
+ import { existsSync as existsSync10, readFileSync as readFileSync8, rmSync as rmSync5, writeFileSync as writeFileSync7 } from "fs";
27002
+ import { join as join13 } from "path";
25017
27003
  var lockPath = null;
25018
27004
  var LockFileSchema = external_exports.object({ pid: external_exports.number().int() });
25019
27005
  function acquireLock(dataDir) {
25020
- lockPath = join9(dataDir, "glassbox.lock");
25021
- if (existsSync8(lockPath)) {
27006
+ lockPath = join13(dataDir, "glassbox.lock");
27007
+ if (existsSync10(lockPath)) {
25022
27008
  try {
25023
27009
  const raw2 = JSON.parse(readFileSync8(lockPath, "utf-8"));
25024
27010
  const contents = LockFileSchema.parse(raw2);
@@ -25190,15 +27176,15 @@ async function updateReviewDiffs(reviewId, newDiffs, headCommit) {
25190
27176
 
25191
27177
  // src/server.ts
25192
27178
  import { serve } from "@hono/node-server";
25193
- import { existsSync as existsSync16, readFileSync as readFileSync20 } from "fs";
25194
- import { Hono as Hono21 } from "hono";
25195
- import { dirname as dirname9, join as join21 } from "path";
27179
+ import { existsSync as existsSync18, readFileSync as readFileSync20 } from "fs";
27180
+ import { Hono as Hono22 } from "hono";
27181
+ import { dirname as dirname12, join as join28 } from "path";
25196
27182
  import { fileURLToPath as fileURLToPath4 } from "url";
25197
27183
 
25198
27184
  // src/channel-config.ts
25199
27185
  init_zod();
25200
- import { existsSync as existsSync9, readFileSync as readFileSync9, writeFileSync as writeFileSync8 } from "fs";
25201
- import { dirname as dirname7, join as join10, resolve as resolve5 } from "path";
27186
+ import { existsSync as existsSync11, readFileSync as readFileSync9, writeFileSync as writeFileSync8 } from "fs";
27187
+ import { dirname as dirname9, join as join14, resolve as resolve7 } from "path";
25202
27188
  import { fileURLToPath as fileURLToPath2 } from "url";
25203
27189
  var MCP_SERVER_KEY = "glassbox-channel";
25204
27190
  var McpConfigSchema = external_exports.object({
@@ -25206,13 +27192,13 @@ var McpConfigSchema = external_exports.object({
25206
27192
  }).loose();
25207
27193
  var HealthResponseSchema = external_exports.object({ ok: external_exports.boolean() });
25208
27194
  function getChannelServerPath() {
25209
- const thisDir = dirname7(fileURLToPath2(import.meta.url));
25210
- const distPath = resolve5(thisDir, "channel.js");
25211
- if (existsSync9(distPath)) {
27195
+ const thisDir = dirname9(fileURLToPath2(import.meta.url));
27196
+ const distPath = resolve7(thisDir, "channel.js");
27197
+ if (existsSync11(distPath)) {
25212
27198
  return { command: process.execPath, args: [distPath] };
25213
27199
  }
25214
- const srcPath = resolve5(thisDir, "channel.ts");
25215
- if (existsSync9(srcPath)) {
27200
+ const srcPath = resolve7(thisDir, "channel.ts");
27201
+ if (existsSync11(srcPath)) {
25216
27202
  return { command: "npx", args: ["tsx", srcPath] };
25217
27203
  }
25218
27204
  return { command: process.execPath, args: [distPath] };
@@ -25222,10 +27208,10 @@ function projectRoot(dataDir) {
25222
27208
  }
25223
27209
  function registerChannel(dataDir) {
25224
27210
  const root = projectRoot(dataDir);
25225
- const mcpPath = join10(root, ".mcp.json");
27211
+ const mcpPath = join14(root, ".mcp.json");
25226
27212
  const { command, args } = getChannelServerPath();
25227
27213
  let config2 = {};
25228
- if (existsSync9(mcpPath)) {
27214
+ if (existsSync11(mcpPath)) {
25229
27215
  try {
25230
27216
  const raw2 = JSON.parse(readFileSync9(mcpPath, "utf-8"));
25231
27217
  const parsed = McpConfigSchema.safeParse(raw2);
@@ -25242,8 +27228,8 @@ function registerChannel(dataDir) {
25242
27228
  }
25243
27229
  function unregisterChannel(dataDir) {
25244
27230
  const root = projectRoot(dataDir);
25245
- const mcpPath = join10(root, ".mcp.json");
25246
- if (!existsSync9(mcpPath)) return;
27231
+ const mcpPath = join14(root, ".mcp.json");
27232
+ if (!existsSync11(mcpPath)) return;
25247
27233
  try {
25248
27234
  const raw2 = JSON.parse(readFileSync9(mcpPath, "utf-8"));
25249
27235
  const parsed = McpConfigSchema.safeParse(raw2);
@@ -25260,7 +27246,7 @@ function unregisterChannel(dataDir) {
25260
27246
  }
25261
27247
  function getChannelPort(dataDir) {
25262
27248
  try {
25263
- const portStr = readFileSync9(join10(dataDir, "channel-port"), "utf-8").trim();
27249
+ const portStr = readFileSync9(join14(dataDir, "channel-port"), "utf-8").trim();
25264
27250
  const port = parseInt(portStr, 10);
25265
27251
  return isNaN(port) ? null : port;
25266
27252
  } catch {
@@ -25281,13 +27267,13 @@ async function isChannelAlive(dataDir) {
25281
27267
  }
25282
27268
  function getChannelSecret(dataDir) {
25283
27269
  try {
25284
- const secret = readFileSync9(join10(dataDir, "channel-secret"), "utf-8").trim();
27270
+ const secret = readFileSync9(join14(dataDir, "channel-secret"), "utf-8").trim();
25285
27271
  return secret === "" ? null : secret;
25286
27272
  } catch {
25287
27273
  return null;
25288
27274
  }
25289
27275
  }
25290
- async function triggerChannel(dataDir, message) {
27276
+ async function triggerChannel(dataDir, message2) {
25291
27277
  const port = getChannelPort(dataDir);
25292
27278
  const secret = getChannelSecret(dataDir);
25293
27279
  if (port === null || secret === null) return false;
@@ -25295,7 +27281,7 @@ async function triggerChannel(dataDir, message) {
25295
27281
  const res = await fetch(`http://127.0.0.1:${port}/trigger`, {
25296
27282
  method: "POST",
25297
27283
  headers: { "X-Glassbox-Secret": secret },
25298
- body: message
27284
+ body: message2
25299
27285
  });
25300
27286
  return res.ok;
25301
27287
  } catch {
@@ -26172,8 +28158,8 @@ function isRetriable(err) {
26172
28158
  return msg.includes("429") || msg.includes("500") || msg.includes("502") || msg.includes("503") || msg.includes("504") || msg.includes("rate_limit");
26173
28159
  }
26174
28160
  function sleep(ms) {
26175
- return new Promise((resolve13) => {
26176
- setTimeout(resolve13, ms);
28161
+ return new Promise((resolve15) => {
28162
+ setTimeout(resolve15, ms);
26177
28163
  });
26178
28164
  }
26179
28165
 
@@ -26217,8 +28203,8 @@ function randomLines(count) {
26217
28203
  return lines.sort((a, b) => a.line - b.line);
26218
28204
  }
26219
28205
  function sleep2(ms) {
26220
- return new Promise((resolve13) => {
26221
- setTimeout(resolve13, ms);
28206
+ return new Promise((resolve15) => {
28207
+ setTimeout(resolve15, ms);
26222
28208
  });
26223
28209
  }
26224
28210
  async function mockRiskAnalysisBatch(files) {
@@ -26348,10 +28334,10 @@ async function executeAnalysis(input) {
26348
28334
  debugLog(`Analysis ${analysisId} completed successfully`);
26349
28335
  await updateAnalysisStatus(analysisId, "completed");
26350
28336
  } catch (err) {
26351
- const message = err instanceof Error ? err.message : "Unknown error";
26352
- console.error(`Analysis failed: ${message}`);
26353
- debugLog(`Analysis ${analysisId} failed: ${message}`);
26354
- await updateAnalysisStatus(analysisId, "failed", message);
28337
+ const message2 = err instanceof Error ? err.message : "Unknown error";
28338
+ console.error(`Analysis failed: ${message2}`);
28339
+ debugLog(`Analysis ${analysisId} failed: ${message2}`);
28340
+ await updateAnalysisStatus(analysisId, "failed", message2);
26355
28341
  } finally {
26356
28342
  canceledAnalyses.delete(analysisId);
26357
28343
  }
@@ -28126,8 +30112,8 @@ function requireSlugParam(c, name) {
28126
30112
  }
28127
30113
  return value;
28128
30114
  }
28129
- function errorResponse(c, message, status = 400) {
28130
- return c.json({ error: message }, status);
30115
+ function errorResponse(c, message2, status = 400) {
30116
+ return c.json({ error: message2 }, status);
28131
30117
  }
28132
30118
 
28133
30119
  // src/utils/resolveReviewId.ts
@@ -28508,7 +30494,7 @@ aiApiRoutes.route("/", aiConfigRoutes);
28508
30494
  aiApiRoutes.route("/", aiAnalysisRoutes);
28509
30495
 
28510
30496
  // src/routes/api.ts
28511
- import { Hono as Hono16 } from "hono";
30497
+ import { Hono as Hono17 } from "hono";
28512
30498
 
28513
30499
  // src/routes/api/annotations.ts
28514
30500
  import { Hono as Hono4 } from "hono";
@@ -28518,8 +30504,8 @@ init_queries();
28518
30504
  init_attachment_queries();
28519
30505
  init_queries();
28520
30506
  init_schemas3();
28521
- import { existsSync as existsSync13, mkdirSync as mkdirSync10, readFileSync as readFileSync13, unlinkSync as unlinkSync2, writeFileSync as writeFileSync11 } from "fs";
28522
- import { join as join14 } from "path";
30507
+ import { existsSync as existsSync15, mkdirSync as mkdirSync10, readFileSync as readFileSync13, unlinkSync as unlinkSync2, writeFileSync as writeFileSync11 } from "fs";
30508
+ import { join as join18 } from "path";
28523
30509
  init_image_metadata();
28524
30510
 
28525
30511
  // src/utils/formatReviewMode.ts
@@ -28814,10 +30800,10 @@ function buildReviewMarkdown(args) {
28814
30800
 
28815
30801
  // src/export/generate.ts
28816
30802
  function deleteReviewExport(reviewId, repoRoot) {
28817
- const exportDir = join14(repoRoot, ".glassbox");
30803
+ const exportDir = join18(repoRoot, ".glassbox");
28818
30804
  for (const ext of ["md", "json"]) {
28819
- const archivePath = join14(exportDir, `review-${reviewId}.${ext}`);
28820
- if (existsSync13(archivePath)) unlinkSync2(archivePath);
30805
+ const archivePath = join18(exportDir, `review-${reviewId}.${ext}`);
30806
+ if (existsSync15(archivePath)) unlinkSync2(archivePath);
28821
30807
  }
28822
30808
  }
28823
30809
  function annotationAnchorLabel(a) {
@@ -28844,7 +30830,7 @@ async function generateReviewExport(reviewId, repoRoot, isCurrent) {
28844
30830
  if (!review) throw new Error("Review not found");
28845
30831
  const files = await getReviewFiles(reviewId);
28846
30832
  const annotations = await getAnnotationsForReview(reviewId);
28847
- const exportDir = join14(repoRoot, ".glassbox");
30833
+ const exportDir = join18(repoRoot, ".glassbox");
28848
30834
  mkdirSync10(exportDir, { recursive: true });
28849
30835
  const modeLabel = formatReviewMode(review.mode, review.mode_args);
28850
30836
  const exportDate = (/* @__PURE__ */ new Date()).toISOString();
@@ -28877,13 +30863,13 @@ async function generateReviewExport(reviewId, repoRoot, isCurrent) {
28877
30863
  resolveDims: resolveImageDims
28878
30864
  });
28879
30865
  const json2 = JSON.stringify(exportData, null, 2);
28880
- const archivePath = join14(exportDir, `review-${review.id}.md`);
30866
+ const archivePath = join18(exportDir, `review-${review.id}.md`);
28881
30867
  writeFileSync11(archivePath, content, "utf-8");
28882
- writeFileSync11(join14(exportDir, `review-${review.id}.json`), json2, "utf-8");
30868
+ writeFileSync11(join18(exportDir, `review-${review.id}.json`), json2, "utf-8");
28883
30869
  if (isCurrent) {
28884
- const latestPath = join14(exportDir, "latest-review.md");
30870
+ const latestPath = join18(exportDir, "latest-review.md");
28885
30871
  writeFileSync11(latestPath, content, "utf-8");
28886
- writeFileSync11(join14(exportDir, "latest-review.json"), json2, "utf-8");
30872
+ writeFileSync11(join18(exportDir, "latest-review.json"), json2, "utf-8");
28887
30873
  return latestPath;
28888
30874
  }
28889
30875
  return archivePath;
@@ -29049,7 +31035,7 @@ function mimeForFilename(filename) {
29049
31035
 
29050
31036
  // src/utils/openOS.ts
29051
31037
  import { execFileSync, spawn } from "child_process";
29052
- import { resolve as resolve6 } from "path";
31038
+ import { resolve as resolve8 } from "path";
29053
31039
  function openOS(target, mode) {
29054
31040
  if (mode === "quicklook") {
29055
31041
  if (process.platform === "darwin") {
@@ -29077,7 +31063,7 @@ function openOS(target, mode) {
29077
31063
  } else if (process.platform === "win32") {
29078
31064
  launchDetached("explorer", ["/select," + target]);
29079
31065
  } else {
29080
- launchDetached("xdg-open", [resolve6(target, "..")]);
31066
+ launchDetached("xdg-open", [resolve8(target, "..")]);
29081
31067
  }
29082
31068
  return;
29083
31069
  }
@@ -29161,8 +31147,8 @@ attachmentsRoutes.get("/attachments/:id/raw", async (c) => {
29161
31147
  const attachment = await getAttachment(idParam.data);
29162
31148
  if (attachment === void 0) return c.text("Not found", 404);
29163
31149
  try {
29164
- const stat = statSync5(attachment.stored_path);
29165
- if (!stat.isFile()) return c.text("Not found", 404);
31150
+ const stat4 = statSync5(attachment.stored_path);
31151
+ if (!stat4.isFile()) return c.text("Not found", 404);
29166
31152
  const bytes = readFileSync14(attachment.stored_path);
29167
31153
  const asciiName = attachment.original_filename.replace(/[^\x20-\x7E]/g, "_").replace(/["\\]/g, "_");
29168
31154
  const utf8Name = encodeURIComponent(attachment.original_filename);
@@ -29230,9 +31216,130 @@ contextRoutes.get("/context/:fileId", async (c) => {
29230
31216
  return c.json({ lines });
29231
31217
  });
29232
31218
 
29233
- // src/routes/api/files.ts
31219
+ // src/routes/api/db-backups.ts
29234
31220
  import { Hono as Hono7 } from "hono";
29235
- import { resolve as resolve7 } from "path";
31221
+
31222
+ // src/db/backups.ts
31223
+ import { readdirSync as readdirSync4, rmSync as rmSync7, statSync as statSync6 } from "fs";
31224
+ import { basename as basename6, dirname as dirname11, join as join19 } from "path";
31225
+ var BACKUP_PREFIX = ".bak-";
31226
+ var QUARANTINE_PREFIX = ".unreadable-";
31227
+ function listDatabaseBackups(dbPath) {
31228
+ return listWithPrefix(dbPath, BACKUP_PREFIX);
31229
+ }
31230
+ function listQuarantinedDirectories(dbPath) {
31231
+ return listWithPrefix(dbPath, QUARANTINE_PREFIX);
31232
+ }
31233
+ function resolvePreservedDirectory(dbPath, name) {
31234
+ if (!isPreservedName(dbPath, name)) return null;
31235
+ const target = join19(dirname11(dbPath), name);
31236
+ try {
31237
+ return statSync6(target).isDirectory() ? target : null;
31238
+ } catch {
31239
+ return null;
31240
+ }
31241
+ }
31242
+ function listWithPrefix(dbPath, prefix) {
31243
+ const parent = dirname11(dbPath);
31244
+ const full = `${basename6(dbPath)}${prefix}`;
31245
+ let entries;
31246
+ try {
31247
+ entries = readdirSync4(parent);
31248
+ } catch {
31249
+ return [];
31250
+ }
31251
+ return entries.filter((name) => name.startsWith(full)).map((name) => {
31252
+ const path = join19(parent, name);
31253
+ return {
31254
+ name,
31255
+ path,
31256
+ bytes: directorySize(path),
31257
+ createdAt: parseStamp(name.slice(full.length))
31258
+ };
31259
+ }).sort((a, b) => a.name < b.name ? 1 : -1);
31260
+ }
31261
+ function isPreservedName(dbPath, name) {
31262
+ if (name !== basename6(name)) return false;
31263
+ const stem = basename6(dbPath);
31264
+ return name.startsWith(`${stem}${BACKUP_PREFIX}`) || name.startsWith(`${stem}${QUARANTINE_PREFIX}`);
31265
+ }
31266
+ function deleteDatabaseBackup(dbPath, name) {
31267
+ const prefix = `${basename6(dbPath)}${BACKUP_PREFIX}`;
31268
+ if (!name.startsWith(prefix)) return false;
31269
+ if (name !== basename6(name)) return false;
31270
+ const target = join19(dirname11(dbPath), name);
31271
+ try {
31272
+ if (!statSync6(target).isDirectory()) return false;
31273
+ rmSync7(target, { recursive: true, force: true });
31274
+ return true;
31275
+ } catch {
31276
+ return false;
31277
+ }
31278
+ }
31279
+ function directorySize(path) {
31280
+ let total = 0;
31281
+ let entries;
31282
+ try {
31283
+ entries = readdirSync4(path);
31284
+ } catch {
31285
+ return 0;
31286
+ }
31287
+ for (const entry of entries) {
31288
+ const child = join19(path, entry);
31289
+ try {
31290
+ const stat4 = statSync6(child);
31291
+ total += stat4.isDirectory() ? directorySize(child) : stat4.size;
31292
+ } catch {
31293
+ }
31294
+ }
31295
+ return total;
31296
+ }
31297
+ function parseStamp(suffix) {
31298
+ const m = /^(\d{4})-(\d{2})-(\d{2})T(\d{2})-(\d{2})-(\d{2})[.-](\d{3})Z$/.exec(suffix);
31299
+ if (m === null) return null;
31300
+ const [, y, mo, d, h, mi, s, ms] = m;
31301
+ const iso = `${y}-${mo}-${d}T${h}:${mi}:${s}.${ms}Z`;
31302
+ return Number.isNaN(Date.parse(iso)) ? null : iso;
31303
+ }
31304
+
31305
+ // src/routes/api/db-backups.ts
31306
+ init_connection();
31307
+ var dbBackupRoutes = new Hono7();
31308
+ dbBackupRoutes.get("/db-backups", (c) => {
31309
+ const dbPath = getDbPath();
31310
+ if (dbPath === null) return c.json({ backups: [], quarantined: [] });
31311
+ return c.json({
31312
+ backups: listDatabaseBackups(dbPath),
31313
+ quarantined: listQuarantinedDirectories(dbPath)
31314
+ });
31315
+ });
31316
+ dbBackupRoutes.delete("/db-backups/:name", (c) => {
31317
+ const name = requireSlugParam(c, "name");
31318
+ if (!name.ok) return name.response;
31319
+ const dbPath = getDbPath();
31320
+ if (dbPath === null) return errorResponse(c, "No data directory is set", 400);
31321
+ if (!deleteDatabaseBackup(dbPath, name.data)) {
31322
+ return errorResponse(c, `No such database backup: ${name.data}`, 404);
31323
+ }
31324
+ return c.json({ ok: true });
31325
+ });
31326
+ dbBackupRoutes.post("/db-backups/:name/reveal", (c) => {
31327
+ const name = requireSlugParam(c, "name");
31328
+ if (!name.ok) return name.response;
31329
+ const dbPath = getDbPath();
31330
+ if (dbPath === null) return errorResponse(c, "No data directory is set", 400);
31331
+ const target = resolvePreservedDirectory(dbPath, name.data);
31332
+ if (target === null) return errorResponse(c, `No such directory: ${name.data}`, 404);
31333
+ try {
31334
+ openOS(target, "reveal");
31335
+ } catch {
31336
+ }
31337
+ return c.json({ ok: true });
31338
+ });
31339
+
31340
+ // src/routes/api/files.ts
31341
+ import { Hono as Hono8 } from "hono";
31342
+ import { resolve as resolve9 } from "path";
29236
31343
  init_queries();
29237
31344
 
29238
31345
  // src/ground-truth/presentation.ts
@@ -29296,7 +31403,7 @@ async function renderPluginSvgSide(mode, filePath, oldPath, side, cwd) {
29296
31403
  }
29297
31404
 
29298
31405
  // src/routes/api/files.ts
29299
- var filesRoutes = new Hono7();
31406
+ var filesRoutes = new Hono8();
29300
31407
  filesRoutes.get("/files", async (c) => {
29301
31408
  const reviewId = resolveReviewId(c);
29302
31409
  const [files, annotationCounts, staleCounts, review] = await Promise.all([
@@ -29337,7 +31444,7 @@ filesRoutes.post("/files/:fileId/reveal", async (c) => {
29337
31444
  const file2 = await getReviewFile(fileId.data);
29338
31445
  if (!file2) return errorResponse(c, "Not found", 404);
29339
31446
  const repoRoot = c.get("repoRoot");
29340
- const fullPath = resolve7(repoRoot, file2.file_path);
31447
+ const fullPath = resolve9(repoRoot, file2.file_path);
29341
31448
  try {
29342
31449
  openOS(fullPath, "reveal");
29343
31450
  } catch (err) {
@@ -29351,7 +31458,7 @@ filesRoutes.get("/files/:fileId/path", async (c) => {
29351
31458
  const file2 = await getReviewFile(fileId.data);
29352
31459
  if (!file2) return errorResponse(c, "Not found", 404);
29353
31460
  const repoRoot = c.get("repoRoot");
29354
- return c.json({ relativePath: file2.file_path, absolutePath: resolve7(repoRoot, file2.file_path) });
31461
+ return c.json({ relativePath: file2.file_path, absolutePath: resolve9(repoRoot, file2.file_path) });
29355
31462
  });
29356
31463
  filesRoutes.post("/files/:fileId/open", async (c) => {
29357
31464
  const fileId = requirePathParam(c, "fileId");
@@ -29359,7 +31466,7 @@ filesRoutes.post("/files/:fileId/open", async (c) => {
29359
31466
  const file2 = await getReviewFile(fileId.data);
29360
31467
  if (!file2) return errorResponse(c, "Not found", 404);
29361
31468
  const repoRoot = c.get("repoRoot");
29362
- const fullPath = resolve7(repoRoot, file2.file_path);
31469
+ const fullPath = resolve9(repoRoot, file2.file_path);
29363
31470
  try {
29364
31471
  openOS(fullPath, "edit");
29365
31472
  } catch (err) {
@@ -29371,13 +31478,13 @@ filesRoutes.post("/files/:fileId/open", async (c) => {
29371
31478
  // src/routes/api/image.ts
29372
31479
  init_connection();
29373
31480
  init_queries();
29374
- import { Hono as Hono8 } from "hono";
31481
+ import { Hono as Hono9 } from "hono";
29375
31482
 
29376
31483
  // src/git/image.ts
29377
31484
  init_lfs();
29378
31485
  import { spawnSync as spawnSync8 } from "child_process";
29379
31486
  import { readFileSync as readFileSync15 } from "fs";
29380
- import { join as join15, resolve as resolve8 } from "path";
31487
+ import { join as join20, resolve as resolve10 } from "path";
29381
31488
  init_image_metadata();
29382
31489
  function getOldRef(mode) {
29383
31490
  switch (mode.type) {
@@ -29448,7 +31555,7 @@ function gitShowFile(ref, filePath, repoRoot) {
29448
31555
  }
29449
31556
  function readWorkingFile(filePath, repoRoot) {
29450
31557
  try {
29451
- const data = readFileSync15(resolve8(repoRoot, filePath));
31558
+ const data = readFileSync15(resolve10(repoRoot, filePath));
29452
31559
  return isLfsPointer(data) ? null : data;
29453
31560
  } catch {
29454
31561
  return null;
@@ -29467,7 +31574,7 @@ function groundTruthEntry(mode, filePath) {
29467
31574
  }
29468
31575
  function getOldImage(mode, filePath, oldPath, repoRoot) {
29469
31576
  if (mode.type === "diff") {
29470
- return readDiskImage(join15(directComparisonRoots(mode).rootA, oldPath ?? filePath));
31577
+ return readDiskImage(join20(directComparisonRoots(mode).rootA, oldPath ?? filePath));
29471
31578
  }
29472
31579
  if (mode.type === "ground-truth") {
29473
31580
  const entry = groundTruthEntry(mode, filePath);
@@ -29487,7 +31594,7 @@ function getOldImage(mode, filePath, oldPath, repoRoot) {
29487
31594
  }
29488
31595
  function getNewImage(mode, filePath, repoRoot) {
29489
31596
  if (mode.type === "diff") {
29490
- return readDiskImage(join15(directComparisonRoots(mode).rootB, filePath));
31597
+ return readDiskImage(join20(directComparisonRoots(mode).rootB, filePath));
29491
31598
  }
29492
31599
  if (mode.type === "ground-truth") {
29493
31600
  const entry = groundTruthEntry(mode, filePath);
@@ -29511,7 +31618,7 @@ function getNewImage(mode, filePath, repoRoot) {
29511
31618
 
29512
31619
  // src/routes/api/image.ts
29513
31620
  init_image_blobs();
29514
- var imageRoutes = new Hono8();
31621
+ var imageRoutes = new Hono9();
29515
31622
  function blobImageSide(fileId, side) {
29516
31623
  const dataDir = getDataDir();
29517
31624
  if (dataDir === null) return null;
@@ -29589,8 +31696,8 @@ imageRoutes.get("/image/:fileId/:side", async (c) => {
29589
31696
  init_queries();
29590
31697
  import { spawnSync as spawnSync9 } from "child_process";
29591
31698
  import { readFileSync as readFileSync16 } from "fs";
29592
- import { Hono as Hono9 } from "hono";
29593
- import { resolve as resolve9 } from "path";
31699
+ import { Hono as Hono10 } from "hono";
31700
+ import { resolve as resolve11 } from "path";
29594
31701
 
29595
31702
  // src/outline/parser.ts
29596
31703
  var BRACE_LANGS = /* @__PURE__ */ new Set([
@@ -29910,7 +32017,7 @@ function pushIndentSymbol(root, stack, sym, indent, lines, lineIdx) {
29910
32017
  }
29911
32018
 
29912
32019
  // src/routes/api/outline.ts
29913
- var outlineRoutes = new Hono9();
32020
+ var outlineRoutes = new Hono10();
29914
32021
  var MAX_REPO_SCAN_FILES = 2e3;
29915
32022
  outlineRoutes.get("/outline/:fileId", async (c) => {
29916
32023
  const repoRoot = c.get("repoRoot");
@@ -29966,7 +32073,7 @@ outlineRoutes.get("/symbol-definition", async (c) => {
29966
32073
  }
29967
32074
  let content = "";
29968
32075
  try {
29969
- content = readFileSync16(resolve9(repoRoot, filePath), "utf-8");
32076
+ content = readFileSync16(resolve11(repoRoot, filePath), "utf-8");
29970
32077
  } catch {
29971
32078
  continue;
29972
32079
  }
@@ -30002,13 +32109,13 @@ function collectDefinitions(symbols, targetName, fileId, filePath, out) {
30002
32109
  }
30003
32110
 
30004
32111
  // src/routes/api/plugins.ts
30005
- import { Hono as Hono10 } from "hono";
32112
+ import { Hono as Hono11 } from "hono";
30006
32113
 
30007
32114
  // src/plugins/available.ts
30008
32115
  init_install();
30009
32116
  init_loader();
30010
- import { existsSync as existsSync14 } from "fs";
30011
- import { join as join16 } from "path";
32117
+ import { existsSync as existsSync16 } from "fs";
32118
+ import { join as join21 } from "path";
30012
32119
 
30013
32120
  // src/plugins/readiness.ts
30014
32121
  import { spawnSync as spawnSync10 } from "child_process";
@@ -30064,7 +32171,7 @@ function listAvailablePlugins(opts) {
30064
32171
  const out = [];
30065
32172
  for (const { manifest } of discoverBundledPlugins(bundledDir)) {
30066
32173
  if (manifest.autoInstall !== false) continue;
30067
- if (existsSync14(join16(userDir, manifest.id))) continue;
32174
+ if (existsSync16(join21(userDir, manifest.id))) continue;
30068
32175
  out.push(describeAvailablePlugin(manifest, opts?.runProbe));
30069
32176
  }
30070
32177
  return out.sort((a, b) => a.name.localeCompare(b.name));
@@ -30079,9 +32186,9 @@ init_install();
30079
32186
  init_install();
30080
32187
  init_loader();
30081
32188
  import { spawnSync as spawnSync11 } from "child_process";
30082
- import { createHash as createHash4 } from "crypto";
32189
+ import { createHash as createHash6 } from "crypto";
30083
32190
  import { cpSync as cpSync2, mkdirSync as mkdirSync11, writeFileSync as writeFileSync12 } from "fs";
30084
- import { join as join17 } from "path";
32191
+ import { join as join24 } from "path";
30085
32192
  var defaultFetchBytes = async (url2) => {
30086
32193
  const res = await fetch(url2);
30087
32194
  if (!res.ok) throw new Error(`HTTP ${String(res.status)}`);
@@ -30099,12 +32206,12 @@ async function runProvisionStep(step, installDir, requirements, deps) {
30099
32206
  try {
30100
32207
  const bytes = await deps.fetchBytes(step.url);
30101
32208
  if (step.sha256 !== void 0) {
30102
- const got = createHash4("sha256").update(bytes).digest("hex");
32209
+ const got = createHash6("sha256").update(bytes).digest("hex");
30103
32210
  if (got.toLowerCase() !== step.sha256.toLowerCase()) {
30104
32211
  return { step: label2, ok: false, skipped: false, detail: `checksum mismatch (expected ${step.sha256})` };
30105
32212
  }
30106
32213
  }
30107
- writeFileSync12(join17(installDir, step.dest), bytes);
32214
+ writeFileSync12(join24(installDir, step.dest), bytes);
30108
32215
  return { step: label2, ok: true, skipped: false, detail: `saved ${step.dest}` };
30109
32216
  } catch (e) {
30110
32217
  return { step: label2, ok: false, skipped: false, detail: e instanceof Error ? e.message : "download failed" };
@@ -30139,7 +32246,7 @@ async function installAvailablePlugin(id, deps = {}) {
30139
32246
  return { id, installed: false, status: "error", requirements: [], provisioned: [], instructions: [], error: "Not a bundled plugin available to install." };
30140
32247
  }
30141
32248
  const manifest = bundled.manifest;
30142
- const dest = join17(userDir, id);
32249
+ const dest = join24(userDir, id);
30143
32250
  try {
30144
32251
  mkdirSync11(userDir, { recursive: true });
30145
32252
  cpSync2(bundled.dir, dest, { recursive: true });
@@ -30160,7 +32267,7 @@ async function installAvailablePlugin(id, deps = {}) {
30160
32267
  // src/routes/api/plugins.ts
30161
32268
  init_loader();
30162
32269
  init_settings();
30163
- var pluginsRoutes = new Hono10();
32270
+ var pluginsRoutes = new Hono11();
30164
32271
  pluginsRoutes.get("/plugins", (c) => {
30165
32272
  return c.json({ plugins: describeInstalledPlugins(c.get("repoRoot")) });
30166
32273
  });
@@ -30242,8 +32349,8 @@ pluginsRoutes.delete("/plugins/:id", async (c) => {
30242
32349
  // src/routes/api/project-settings.ts
30243
32350
  init_project_settings();
30244
32351
  init_project_settings_store();
30245
- import { Hono as Hono11 } from "hono";
30246
- var projectSettingsRoutes = new Hono11();
32352
+ import { Hono as Hono12 } from "hono";
32353
+ var projectSettingsRoutes = new Hono12();
30247
32354
  projectSettingsRoutes.get("/project-settings", (c) => {
30248
32355
  return c.json(readProjectSettings(c.get("repoRoot")));
30249
32356
  });
@@ -30260,10 +32367,10 @@ projectSettingsRoutes.patch("/project-settings", async (c) => {
30260
32367
  // src/routes/api/review-notes.ts
30261
32368
  init_store();
30262
32369
  init_lfs();
30263
- import { readFileSync as readFileSync17, statSync as statSync6 } from "fs";
30264
- import { Hono as Hono12 } from "hono";
30265
- import { extname as extname2, relative as relative2, resolve as resolve10 } from "path";
30266
- var reviewNotesRoutes = new Hono12();
32370
+ import { readFileSync as readFileSync17, statSync as statSync7 } from "fs";
32371
+ import { Hono as Hono13 } from "hono";
32372
+ import { extname as extname2, relative as relative2, resolve as resolve12 } from "path";
32373
+ var reviewNotesRoutes = new Hono13();
30267
32374
  var ARTIFACT_SERVE_MAX_BYTES = 1e7;
30268
32375
  var IMAGE_CONTENT_TYPES = {
30269
32376
  ".png": "image/png",
@@ -30278,15 +32385,15 @@ reviewNotesRoutes.get("/review-notes/artifact", (c) => {
30278
32385
  const file2 = c.req.query("file");
30279
32386
  if (file2 === void 0 || file2 === "") return c.text("Missing file", 400);
30280
32387
  const repoRoot = c.get("repoRoot");
30281
- const abs = resolve10(repoRoot, file2);
32388
+ const abs = resolve12(repoRoot, file2);
30282
32389
  const rel = relative2(repoRoot, abs);
30283
32390
  if (rel === "" || rel.startsWith("..") || rel.startsWith("/")) return c.text("Forbidden", 403);
30284
32391
  const ext = extname2(abs).toLowerCase();
30285
32392
  if (!(ext in IMAGE_CONTENT_TYPES)) return c.text("Unsupported artifact type", 415);
30286
32393
  const contentType = IMAGE_CONTENT_TYPES[ext];
30287
32394
  try {
30288
- const stat = statSync6(abs);
30289
- if (!stat.isFile() || stat.size > ARTIFACT_SERVE_MAX_BYTES) return c.text("Not found", 404);
32395
+ const stat4 = statSync7(abs);
32396
+ if (!stat4.isFile() || stat4.size > ARTIFACT_SERVE_MAX_BYTES) return c.text("Not found", 404);
30290
32397
  const body = readFileSync17(abs);
30291
32398
  if (isLfsPointer(body)) return c.text("Not found", 404);
30292
32399
  return c.body(body, 200, { "Content-Type": contentType });
@@ -30305,24 +32412,24 @@ reviewNotesRoutes.delete("/review-notes/:guid", (c) => {
30305
32412
 
30306
32413
  // src/routes/api/reviews.ts
30307
32414
  init_queries();
30308
- import { Hono as Hono13 } from "hono";
32415
+ import { Hono as Hono14 } from "hono";
30309
32416
 
30310
32417
  // src/export/on-complete-hook.ts
30311
32418
  import { spawn as spawn2 } from "child_process";
30312
32419
  import { appendFileSync } from "fs";
30313
- import { join as join18 } from "path";
32420
+ import { join as join25 } from "path";
30314
32421
  function runOnCompleteHook(command, ctx) {
30315
32422
  if (command === null || command.trim() === "") {
30316
32423
  return Promise.resolve({ ran: false, ok: true, exitCode: 0 });
30317
32424
  }
30318
- const logPath = join18(ctx.repoRoot, ".glassbox", "on-complete.log");
32425
+ const logPath = join25(ctx.repoRoot, ".glassbox", "on-complete.log");
30319
32426
  const log = (s) => {
30320
32427
  try {
30321
32428
  appendFileSync(logPath, s);
30322
32429
  } catch {
30323
32430
  }
30324
32431
  };
30325
- return new Promise((resolve13) => {
32432
+ return new Promise((resolve15) => {
30326
32433
  log(`
30327
32434
  === on-complete ${(/* @__PURE__ */ new Date()).toISOString()} \u2014 review ${ctx.reviewId} ===
30328
32435
  $ ${command}
@@ -30344,7 +32451,7 @@ $ ${command}
30344
32451
  const error51 = err instanceof Error ? err.message : String(err);
30345
32452
  log(`spawn failed: ${error51}
30346
32453
  `);
30347
- resolve13({ ran: true, ok: false, exitCode: null, error: error51 });
32454
+ resolve15({ ran: true, ok: false, exitCode: null, error: error51 });
30348
32455
  return;
30349
32456
  }
30350
32457
  child.stdout.on("data", (d) => {
@@ -30356,19 +32463,19 @@ $ ${command}
30356
32463
  child.on("error", (err) => {
30357
32464
  log(`error: ${err.message}
30358
32465
  `);
30359
- resolve13({ ran: true, ok: false, exitCode: null, error: err.message });
32466
+ resolve15({ ran: true, ok: false, exitCode: null, error: err.message });
30360
32467
  });
30361
32468
  child.on("close", (code) => {
30362
32469
  log(`exited with code ${String(code)}
30363
32470
  `);
30364
- resolve13({ ran: true, ok: code === 0, exitCode: code });
32471
+ resolve15({ ran: true, ok: code === 0, exitCode: code });
30365
32472
  });
30366
32473
  });
30367
32474
  }
30368
32475
 
30369
32476
  // src/routes/api/reviews.ts
30370
32477
  init_plugins();
30371
- var reviewsRoutes = new Hono13();
32478
+ var reviewsRoutes = new Hono14();
30372
32479
  reviewsRoutes.get("/reviews", async (c) => {
30373
32480
  const repoRoot = c.get("repoRoot");
30374
32481
  const reviews = await listReviews(repoRoot);
@@ -30460,9 +32567,9 @@ reviewsRoutes.post("/reviews/delete-all", async (c) => {
30460
32567
 
30461
32568
  // src/routes/api/share-prompt.ts
30462
32569
  init_zod();
30463
- import { Hono as Hono14 } from "hono";
32570
+ import { Hono as Hono15 } from "hono";
30464
32571
  init_global_config();
30465
- var sharePromptRoutes = new Hono14();
32572
+ var sharePromptRoutes = new Hono15();
30466
32573
  var SharePromptShapeSchema = external_exports.object({
30467
32574
  dismissedAt: external_exports.number().nullable().optional(),
30468
32575
  totalOpenMs: external_exports.number().optional()
@@ -30501,8 +32608,8 @@ sharePromptRoutes.post("/share-prompt/tick", async (c) => {
30501
32608
  });
30502
32609
 
30503
32610
  // src/routes/api/system.ts
30504
- import { Hono as Hono15 } from "hono";
30505
- var systemRoutes = new Hono15();
32611
+ import { Hono as Hono16 } from "hono";
32612
+ var systemRoutes = new Hono16();
30506
32613
  systemRoutes.post("/open-external", async (c) => {
30507
32614
  const parsed = await parseBody(c, OpenExternalReqSchema);
30508
32615
  if (!parsed.ok) return parsed.response;
@@ -30514,7 +32621,7 @@ systemRoutes.post("/open-external", async (c) => {
30514
32621
  });
30515
32622
 
30516
32623
  // src/routes/api.ts
30517
- var apiRoutes = new Hono16();
32624
+ var apiRoutes = new Hono17();
30518
32625
  apiRoutes.route("/", reviewsRoutes);
30519
32626
  apiRoutes.route("/", filesRoutes);
30520
32627
  apiRoutes.route("/", annotationsRoutes);
@@ -30527,21 +32634,22 @@ apiRoutes.route("/", imageRoutes);
30527
32634
  apiRoutes.route("/", reviewNotesRoutes);
30528
32635
  apiRoutes.route("/", sharePromptRoutes);
30529
32636
  apiRoutes.route("/", systemRoutes);
32637
+ apiRoutes.route("/", dbBackupRoutes);
30530
32638
 
30531
32639
  // src/routes/channel-api.ts
30532
32640
  import { spawnSync as spawnSync12 } from "child_process";
30533
32641
  import { mkdirSync as mkdirSync12 } from "fs";
30534
- import { Hono as Hono17 } from "hono";
30535
- import { join as join19 } from "path";
32642
+ import { Hono as Hono18 } from "hono";
32643
+ import { join as join26 } from "path";
30536
32644
  init_global_config();
30537
32645
  init_compareVersions();
30538
32646
  var MIN_CLAUDE_VERSION = "2.1.80";
30539
- var channelApiRoutes = new Hono17();
32647
+ var channelApiRoutes = new Hono18();
30540
32648
  channelApiRoutes.get("/status", async (c) => {
30541
32649
  const config2 = readGlobalConfig();
30542
32650
  const enabled = config2.channelEnabled === true;
30543
32651
  const repoRoot = c.get("repoRoot");
30544
- const dataDir = join19(repoRoot, ".glassbox");
32652
+ const dataDir = join26(repoRoot, ".glassbox");
30545
32653
  const connected = enabled ? await isChannelAlive(dataDir) : false;
30546
32654
  return c.json({ enabled, connected });
30547
32655
  });
@@ -30550,7 +32658,7 @@ channelApiRoutes.post("/enable", (c) => {
30550
32658
  config2.channelEnabled = true;
30551
32659
  });
30552
32660
  const repoRoot = c.get("repoRoot");
30553
- const dataDir = join19(repoRoot, ".glassbox");
32661
+ const dataDir = join26(repoRoot, ".glassbox");
30554
32662
  mkdirSync12(dataDir, { recursive: true });
30555
32663
  registerChannel(dataDir);
30556
32664
  return c.json({ ok: true });
@@ -30560,7 +32668,7 @@ channelApiRoutes.post("/disable", (c) => {
30560
32668
  config2.channelEnabled = false;
30561
32669
  });
30562
32670
  const repoRoot = c.get("repoRoot");
30563
- const dataDir = join19(repoRoot, ".glassbox");
32671
+ const dataDir = join26(repoRoot, ".glassbox");
30564
32672
  unregisterChannel(dataDir);
30565
32673
  return c.json({ ok: true });
30566
32674
  });
@@ -30568,7 +32676,7 @@ channelApiRoutes.post("/trigger", async (c) => {
30568
32676
  const parsed = await parseBody(c, TriggerChannelReqSchema);
30569
32677
  if (!parsed.ok) return parsed.response;
30570
32678
  const repoRoot = c.get("repoRoot");
30571
- const dataDir = join19(repoRoot, ".glassbox");
32679
+ const dataDir = join26(repoRoot, ".glassbox");
30572
32680
  const sent = await triggerChannel(dataDir, parsed.data.message);
30573
32681
  if (!sent) {
30574
32682
  return errorResponse(c, "Channel not connected", 503);
@@ -30592,13 +32700,13 @@ channelApiRoutes.get("/claude-check", (c) => {
30592
32700
  });
30593
32701
 
30594
32702
  // src/routes/difftool-api.ts
30595
- import { Hono as Hono18 } from "hono";
32703
+ import { Hono as Hono19 } from "hono";
30596
32704
  init_connection();
30597
32705
  init_queries();
30598
32706
  init_session();
30599
32707
  init_difftool();
30600
32708
  init_image_blobs();
30601
- var difftoolApiRoutes = new Hono18();
32709
+ var difftoolApiRoutes = new Hono19();
30602
32710
  difftoolApiRoutes.get("/status", (c) => {
30603
32711
  return c.json(getDifftoolStatus("global"));
30604
32712
  });
@@ -30660,9 +32768,9 @@ difftoolApiRoutes.get("/hold", (c) => {
30660
32768
  const session2 = getDifftoolSession();
30661
32769
  if (session2 === null) return c.json({ ended: true });
30662
32770
  noteDifftoolActivity();
30663
- return new Promise((resolve13) => {
32771
+ return new Promise((resolve15) => {
30664
32772
  addDifftoolHold(() => {
30665
- resolve13(c.json({ ended: true }));
32773
+ resolve15(c.json({ ended: true }));
30666
32774
  });
30667
32775
  c.req.raw.signal.addEventListener("abort", () => {
30668
32776
  endDifftoolSession();
@@ -30676,8 +32784,8 @@ difftoolApiRoutes.post("/end", (c) => {
30676
32784
 
30677
32785
  // src/routes/pages.tsx
30678
32786
  import { readFileSync as readFileSync19 } from "fs";
30679
- import { Hono as Hono19 } from "hono";
30680
- import { relative as relative3, resolve as resolve11 } from "path";
32787
+ import { Hono as Hono20 } from "hono";
32788
+ import { relative as relative3, resolve as resolve13 } from "path";
30681
32789
 
30682
32790
  // src/components/diffView.tsx
30683
32791
  import { raw } from "kerfjs";
@@ -30999,8 +33107,8 @@ function renderBlocks(lines, depth, ctx) {
30999
33107
  i++;
31000
33108
  continue;
31001
33109
  }
31002
- const quote = QUOTE_RE.exec(line);
31003
- if (quote !== null) {
33110
+ const quote2 = QUOTE_RE.exec(line);
33111
+ if (quote2 !== null) {
31004
33112
  const inner = [];
31005
33113
  while (i < lines.length) {
31006
33114
  const m = QUOTE_RE.exec(lines[i]);
@@ -31636,7 +33744,7 @@ function ReviewNoteRows({ notes, repliesByNote }) {
31636
33744
  className: "ai-note-artifact-img",
31637
33745
  loading: "lazy",
31638
33746
  alt: a.uri,
31639
- draggable: false,
33747
+ draggable: "false",
31640
33748
  src: raw(`data:image/svg+xml;utf8,${encodeURIComponent(a.renderedSvg)}`)
31641
33749
  }
31642
33750
  ) })
@@ -31664,7 +33772,7 @@ function ReviewNoteRows({ notes, repliesByNote }) {
31664
33772
  className: "ai-note-artifact-img",
31665
33773
  loading: "lazy",
31666
33774
  alt: a.uri,
31667
- draggable: false,
33775
+ draggable: "false",
31668
33776
  "data-artifact-uri": a.uri,
31669
33777
  title: "Drag to mark a region, or click to view full screen",
31670
33778
  src: `/api/review-notes/artifact?file=${encodeURIComponent(a.uri)}`
@@ -31693,7 +33801,7 @@ function AnnotationItem({ annotation: a }) {
31693
33801
  "data-is-stale": a.is_stale ? "true" : void 0,
31694
33802
  "data-region-data": a.region_data ?? void 0,
31695
33803
  children: [
31696
- /* @__PURE__ */ jsx4("span", { className: "annotation-drag-handle", draggable: true, title: "Drag to move", children: /* @__PURE__ */ jsx4(IconGripVertical, {}) }),
33804
+ /* @__PURE__ */ jsx4("span", { className: "annotation-drag-handle", draggable: "true", title: "Drag to move", children: /* @__PURE__ */ jsx4(IconGripVertical, {}) }),
31697
33805
  /* @__PURE__ */ jsx4("span", { className: `annotation-category category-${a.category}`, "data-action": "reclassify", children: a.category }),
31698
33806
  a.reply_to_note_id !== null ? /* @__PURE__ */ jsxs4("span", { className: "annotation-reply-tag", title: "Reply to an AI review note", children: [
31699
33807
  /* @__PURE__ */ jsx4(IconCornerDownRight, {}),
@@ -31718,10 +33826,10 @@ function AnnotationRows({ annotations }) {
31718
33826
 
31719
33827
  // src/themes/config.ts
31720
33828
  init_zod();
31721
- import { existsSync as existsSync15, mkdirSync as mkdirSync13, readdirSync as readdirSync4, readFileSync as readFileSync18, unlinkSync as unlinkSync3, writeFileSync as writeFileSync13 } from "fs";
31722
- import { join as join20 } from "path";
33829
+ import { existsSync as existsSync17, mkdirSync as mkdirSync13, readdirSync as readdirSync5, readFileSync as readFileSync18, unlinkSync as unlinkSync3, writeFileSync as writeFileSync13 } from "fs";
33830
+ import { join as join27 } from "path";
31723
33831
  init_global_config();
31724
- var THEMES_DIR = join20(GLOBAL_CONFIG_DIR, "themes");
33832
+ var THEMES_DIR = join27(GLOBAL_CONFIG_DIR, "themes");
31725
33833
  var ThemeConfigSliceSchema = external_exports.object({ active: external_exports.string().optional() }).loose();
31726
33834
  function getActiveThemeId() {
31727
33835
  const config2 = readGlobalConfig();
@@ -31745,11 +33853,11 @@ function readStoredTheme(filePath) {
31745
33853
  }
31746
33854
  }
31747
33855
  function loadCustomThemes() {
31748
- if (!existsSync15(THEMES_DIR)) return [];
33856
+ if (!existsSync17(THEMES_DIR)) return [];
31749
33857
  const themes = [];
31750
33858
  try {
31751
- for (const file2 of readdirSync4(THEMES_DIR).filter((f) => f.endsWith(".json"))) {
31752
- const theme = readStoredTheme(join20(THEMES_DIR, file2));
33859
+ for (const file2 of readdirSync5(THEMES_DIR).filter((f) => f.endsWith(".json"))) {
33860
+ const theme = readStoredTheme(join27(THEMES_DIR, file2));
31753
33861
  if (theme !== void 0) themes.push(theme);
31754
33862
  }
31755
33863
  } catch {
@@ -31758,18 +33866,18 @@ function loadCustomThemes() {
31758
33866
  }
31759
33867
  function saveCustomTheme(theme) {
31760
33868
  mkdirSync13(THEMES_DIR, { recursive: true });
31761
- const filePath = join20(THEMES_DIR, `${theme.id}.json`);
33869
+ const filePath = join27(THEMES_DIR, `${theme.id}.json`);
31762
33870
  writeFileSync13(filePath, JSON.stringify(theme, null, 2), "utf-8");
31763
33871
  }
31764
33872
  function deleteCustomTheme(id) {
31765
- const filePath = join20(THEMES_DIR, `${id}.json`);
31766
- if (existsSync15(filePath)) {
33873
+ const filePath = join27(THEMES_DIR, `${id}.json`);
33874
+ if (existsSync17(filePath)) {
31767
33875
  unlinkSync3(filePath);
31768
33876
  }
31769
33877
  }
31770
33878
  function getCustomTheme(id) {
31771
- const filePath = join20(THEMES_DIR, `${id}.json`);
31772
- if (!existsSync15(filePath)) return void 0;
33879
+ const filePath = join27(THEMES_DIR, `${id}.json`);
33880
+ if (!existsSync17(filePath)) return void 0;
31773
33881
  return readStoredTheme(filePath);
31774
33882
  }
31775
33883
  function getAllThemes() {
@@ -32114,7 +34222,7 @@ function reanchorReviewNotes(notes, diff) {
32114
34222
  // src/routes/pages.tsx
32115
34223
  init_store();
32116
34224
  import { Fragment as Fragment2, jsx as jsx9, jsxs as jsxs9 } from "kerfjs/jsx-runtime";
32117
- var pageRoutes = new Hono19();
34225
+ var pageRoutes = new Hono20();
32118
34226
  pageRoutes.get("/", async (c) => {
32119
34227
  const reviewId = c.get("reviewId");
32120
34228
  const review = await getReview(reviewId);
@@ -32242,7 +34350,7 @@ pageRoutes.get("/file-raw", (c) => {
32242
34350
  const filePath = c.req.query("path");
32243
34351
  if (filePath === void 0 || filePath === "") return c.text("Missing path", 400);
32244
34352
  const repoRoot = c.get("repoRoot");
32245
- const abs = resolve11(repoRoot, filePath);
34353
+ const abs = resolve13(repoRoot, filePath);
32246
34354
  const rel = relative3(repoRoot, abs);
32247
34355
  if (rel === "" || rel.startsWith("..") || rel.startsWith("/")) return c.text("Forbidden", 403);
32248
34356
  let content;
@@ -32303,8 +34411,8 @@ pageRoutes.get("/history", async (c) => {
32303
34411
  });
32304
34412
 
32305
34413
  // src/routes/theme-api.ts
32306
- import { Hono as Hono20 } from "hono";
32307
- var themeApiRoutes = new Hono20();
34414
+ import { Hono as Hono21 } from "hono";
34415
+ var themeApiRoutes = new Hono21();
32308
34416
  themeApiRoutes.get("/", (c) => {
32309
34417
  const themes = getAllThemes();
32310
34418
  const activeId = getActiveThemeId();
@@ -32415,10 +34523,10 @@ themeApiRoutes.delete("/:id", (c) => {
32415
34523
  // src/server.ts
32416
34524
  var MAX_PORT_ATTEMPTS = 20;
32417
34525
  function tryServe(appFetch, port) {
32418
- return new Promise((resolve13, reject) => {
34526
+ return new Promise((resolve15, reject) => {
32419
34527
  const server = serve({ fetch: appFetch, port, hostname: "127.0.0.1" });
32420
34528
  server.on("listening", () => {
32421
- resolve13({ port, server });
34529
+ resolve15({ port, server });
32422
34530
  });
32423
34531
  server.on("error", (err) => {
32424
34532
  reject(err);
@@ -32426,7 +34534,7 @@ function tryServe(appFetch, port) {
32426
34534
  });
32427
34535
  }
32428
34536
  async function startServer(port, reviewId, repoRoot, options) {
32429
- const app = new Hono21();
34537
+ const app = new Hono22();
32430
34538
  const onCompleteCommand = options?.onComplete ?? null;
32431
34539
  app.use("*", async (c, next) => {
32432
34540
  c.set("reviewId", reviewId);
@@ -32435,22 +34543,22 @@ async function startServer(port, reviewId, repoRoot, options) {
32435
34543
  c.set("onCompleteCommand", onCompleteCommand);
32436
34544
  await next();
32437
34545
  });
32438
- const selfDir = dirname9(fileURLToPath4(import.meta.url));
32439
- const distDir = existsSync16(join21(selfDir, "client", "styles.css")) ? join21(selfDir, "client") : join21(selfDir, "..", "dist", "client");
34546
+ const selfDir = dirname12(fileURLToPath4(import.meta.url));
34547
+ const distDir = existsSync18(join28(selfDir, "client", "styles.css")) ? join28(selfDir, "client") : join28(selfDir, "..", "dist", "client");
32440
34548
  app.get("/static/styles.css", (c) => {
32441
- const css = readFileSync20(join21(distDir, "styles.css"), "utf-8");
34549
+ const css = readFileSync20(join28(distDir, "styles.css"), "utf-8");
32442
34550
  return c.text(css, 200, { "Content-Type": "text/css", "Cache-Control": "no-cache" });
32443
34551
  });
32444
34552
  app.get("/static/app.js", (c) => {
32445
- const js = readFileSync20(join21(distDir, "app.global.js"), "utf-8");
34553
+ const js = readFileSync20(join28(distDir, "app.global.js"), "utf-8");
32446
34554
  return c.text(js, 200, { "Content-Type": "application/javascript", "Cache-Control": "no-cache" });
32447
34555
  });
32448
34556
  app.get("/static/history.js", (c) => {
32449
- const js = readFileSync20(join21(distDir, "history.global.js"), "utf-8");
34557
+ const js = readFileSync20(join28(distDir, "history.global.js"), "utf-8");
32450
34558
  return c.text(js, 200, { "Content-Type": "application/javascript", "Cache-Control": "no-cache" });
32451
34559
  });
32452
34560
  app.get("/favicon.svg", (c) => {
32453
- const svg = readFileSync20(join21(distDir, "favicon.svg"), "utf-8");
34561
+ const svg = readFileSync20(join28(distDir, "favicon.svg"), "utf-8");
32454
34562
  return c.body(svg, 200, { "Content-Type": "image/svg+xml", "Cache-Control": "no-cache" });
32455
34563
  });
32456
34564
  app.get("/favicon.ico", (c) => c.body(null, 204));
@@ -32494,7 +34602,7 @@ async function startServer(port, reviewId, repoRoot, options) {
32494
34602
  try {
32495
34603
  const globalConfig2 = readGlobalConfig();
32496
34604
  if (globalConfig2.channelEnabled === true) {
32497
- const dataDir = join21(repoRoot, ".glassbox");
34605
+ const dataDir = join28(repoRoot, ".glassbox");
32498
34606
  registerChannel(dataDir);
32499
34607
  }
32500
34608
  } catch {
@@ -32509,8 +34617,8 @@ async function startServer(port, reviewId, repoRoot, options) {
32509
34617
  }
32510
34618
 
32511
34619
  // src/skills.ts
32512
- import { existsSync as existsSync17, mkdirSync as mkdirSync14, readFileSync as readFileSync21, writeFileSync as writeFileSync14 } from "fs";
32513
- import { join as join22 } from "path";
34620
+ import { existsSync as existsSync19, mkdirSync as mkdirSync14, readFileSync as readFileSync21, writeFileSync as writeFileSync14 } from "fs";
34621
+ import { join as join29 } from "path";
32514
34622
  var SKILL_VERSION = 1;
32515
34623
  function versionHeader() {
32516
34624
  return `<!-- glassbox-skill-version: ${SKILL_VERSION} -->`;
@@ -32521,7 +34629,7 @@ function parseVersionHeader(content) {
32521
34629
  return parseInt(match[1], 10);
32522
34630
  }
32523
34631
  function updateFile(path, content) {
32524
- if (existsSync17(path)) {
34632
+ if (existsSync19(path)) {
32525
34633
  const existing = readFileSync21(path, "utf-8");
32526
34634
  const version2 = parseVersionHeader(existing);
32527
34635
  if (version2 !== null && version2 >= SKILL_VERSION) {
@@ -32548,7 +34656,7 @@ function skillBody() {
32548
34656
  ].join("\n");
32549
34657
  }
32550
34658
  function ensureClaudeSkills(cwd) {
32551
- const dir = join22(cwd, ".claude", "skills", "glassbox");
34659
+ const dir = join29(cwd, ".claude", "skills", "glassbox");
32552
34660
  mkdirSync14(dir, { recursive: true });
32553
34661
  const content = [
32554
34662
  "---",
@@ -32561,10 +34669,10 @@ function ensureClaudeSkills(cwd) {
32561
34669
  skillBody(),
32562
34670
  ""
32563
34671
  ].join("\n");
32564
- return updateFile(join22(dir, "SKILL.md"), content);
34672
+ return updateFile(join29(dir, "SKILL.md"), content);
32565
34673
  }
32566
34674
  function ensureCursorRules(cwd) {
32567
- const rulesDir = join22(cwd, ".cursor", "rules");
34675
+ const rulesDir = join29(cwd, ".cursor", "rules");
32568
34676
  mkdirSync14(rulesDir, { recursive: true });
32569
34677
  const content = [
32570
34678
  "---",
@@ -32576,10 +34684,10 @@ function ensureCursorRules(cwd) {
32576
34684
  skillBody(),
32577
34685
  ""
32578
34686
  ].join("\n");
32579
- return updateFile(join22(rulesDir, "glassbox.mdc"), content);
34687
+ return updateFile(join29(rulesDir, "glassbox.mdc"), content);
32580
34688
  }
32581
34689
  function ensureCopilotPrompts(cwd) {
32582
- const promptsDir = join22(cwd, ".github", "prompts");
34690
+ const promptsDir = join29(cwd, ".github", "prompts");
32583
34691
  mkdirSync14(promptsDir, { recursive: true });
32584
34692
  const content = [
32585
34693
  "---",
@@ -32590,10 +34698,10 @@ function ensureCopilotPrompts(cwd) {
32590
34698
  skillBody(),
32591
34699
  ""
32592
34700
  ].join("\n");
32593
- return updateFile(join22(promptsDir, "glassbox.prompt.md"), content);
34701
+ return updateFile(join29(promptsDir, "glassbox.prompt.md"), content);
32594
34702
  }
32595
34703
  function ensureWindsurfRules(cwd) {
32596
- const rulesDir = join22(cwd, ".windsurf", "rules");
34704
+ const rulesDir = join29(cwd, ".windsurf", "rules");
32597
34705
  mkdirSync14(rulesDir, { recursive: true });
32598
34706
  const content = [
32599
34707
  "---",
@@ -32605,21 +34713,21 @@ function ensureWindsurfRules(cwd) {
32605
34713
  skillBody(),
32606
34714
  ""
32607
34715
  ].join("\n");
32608
- return updateFile(join22(rulesDir, "glassbox.md"), content);
34716
+ return updateFile(join29(rulesDir, "glassbox.md"), content);
32609
34717
  }
32610
34718
  function ensureSkills() {
32611
34719
  const cwd = process.cwd();
32612
34720
  const platforms = [];
32613
- if (existsSync17(join22(cwd, ".claude"))) {
34721
+ if (existsSync19(join29(cwd, ".claude"))) {
32614
34722
  if (ensureClaudeSkills(cwd)) platforms.push("Claude Code");
32615
34723
  }
32616
- if (existsSync17(join22(cwd, ".cursor"))) {
34724
+ if (existsSync19(join29(cwd, ".cursor"))) {
32617
34725
  if (ensureCursorRules(cwd)) platforms.push("Cursor");
32618
34726
  }
32619
- if (existsSync17(join22(cwd, ".github", "prompts")) || existsSync17(join22(cwd, ".github", "copilot-instructions.md"))) {
34727
+ if (existsSync19(join29(cwd, ".github", "prompts")) || existsSync19(join29(cwd, ".github", "copilot-instructions.md"))) {
32620
34728
  if (ensureCopilotPrompts(cwd)) platforms.push("GitHub Copilot");
32621
34729
  }
32622
- if (existsSync17(join22(cwd, ".windsurf"))) {
34730
+ if (existsSync19(join29(cwd, ".windsurf"))) {
32623
34731
  if (ensureWindsurfRules(cwd)) platforms.push("Windsurf");
32624
34732
  }
32625
34733
  return platforms;
@@ -32629,17 +34737,17 @@ function ensureSkills() {
32629
34737
  init_zod();
32630
34738
  init_app_version();
32631
34739
  init_compareVersions();
32632
- import { existsSync as existsSync18, mkdirSync as mkdirSync15, readFileSync as readFileSync22, writeFileSync as writeFileSync15 } from "fs";
34740
+ import { existsSync as existsSync20, mkdirSync as mkdirSync15, readFileSync as readFileSync22, writeFileSync as writeFileSync15 } from "fs";
32633
34741
  import { get } from "https";
32634
- import { homedir as homedir2 } from "os";
32635
- import { join as join23 } from "path";
32636
- var DATA_DIR = join23(homedir2(), ".glassbox");
32637
- var CHECK_FILE = join23(DATA_DIR, "last-update-check");
34742
+ import { homedir as homedir4 } from "os";
34743
+ import { join as join30 } from "path";
34744
+ var DATA_DIR = join30(homedir4(), ".glassbox");
34745
+ var CHECK_FILE = join30(DATA_DIR, "last-update-check");
32638
34746
  var PACKAGE_NAME = "glassbox";
32639
34747
  var VersionPayloadSchema2 = external_exports.object({ version: external_exports.string() });
32640
34748
  function getLastCheckDate() {
32641
34749
  try {
32642
- if (existsSync18(CHECK_FILE)) {
34750
+ if (existsSync20(CHECK_FILE)) {
32643
34751
  return readFileSync22(CHECK_FILE, "utf-8").trim();
32644
34752
  }
32645
34753
  } catch {
@@ -32657,10 +34765,10 @@ function isFirstUseToday() {
32657
34765
  return last !== today;
32658
34766
  }
32659
34767
  function fetchLatestVersion() {
32660
- return new Promise((resolve13) => {
34768
+ return new Promise((resolve15) => {
32661
34769
  const req = get(`https://registry.npmjs.org/${PACKAGE_NAME}/latest`, { timeout: 5e3 }, (res) => {
32662
34770
  if (res.statusCode !== 200) {
32663
- resolve13(null);
34771
+ resolve15(null);
32664
34772
  return;
32665
34773
  }
32666
34774
  let data = "";
@@ -32670,18 +34778,18 @@ function fetchLatestVersion() {
32670
34778
  res.on("end", () => {
32671
34779
  try {
32672
34780
  const raw2 = JSON.parse(data);
32673
- resolve13(VersionPayloadSchema2.parse(raw2).version);
34781
+ resolve15(VersionPayloadSchema2.parse(raw2).version);
32674
34782
  } catch {
32675
- resolve13(null);
34783
+ resolve15(null);
32676
34784
  }
32677
34785
  });
32678
34786
  });
32679
34787
  req.on("error", () => {
32680
- resolve13(null);
34788
+ resolve15(null);
32681
34789
  });
32682
34790
  req.on("timeout", () => {
32683
34791
  req.destroy();
32684
- resolve13(null);
34792
+ resolve15(null);
32685
34793
  });
32686
34794
  });
32687
34795
  }
@@ -32838,7 +34946,7 @@ function parseArgs(argv) {
32838
34946
  console.error("--diff requires two paths: --diff <pathA> <pathB>");
32839
34947
  process.exit(1);
32840
34948
  }
32841
- mode = { type: "diff", pathA: resolve12(args[++i]), pathB: resolve12(args[++i]) };
34949
+ mode = { type: "diff", pathA: resolve14(args[++i]), pathB: resolve14(args[++i]) };
32842
34950
  break;
32843
34951
  }
32844
34952
  case "--ground-truth": {
@@ -32846,7 +34954,7 @@ function parseArgs(argv) {
32846
34954
  console.error("--ground-truth requires a manifest path: --ground-truth <manifest.json>");
32847
34955
  process.exit(1);
32848
34956
  }
32849
- mode = { type: "ground-truth", manifestPath: resolve12(args[++i]), comparisons: [] };
34957
+ mode = { type: "ground-truth", manifestPath: resolve14(args[++i]), comparisons: [] };
32850
34958
  break;
32851
34959
  }
32852
34960
  case "--port": {
@@ -32860,7 +34968,7 @@ function parseArgs(argv) {
32860
34968
  break;
32861
34969
  }
32862
34970
  case "--data-dir":
32863
- dataDir = resolve12(requireValue("--data-dir"));
34971
+ dataDir = resolve14(requireValue("--data-dir"));
32864
34972
  break;
32865
34973
  case "--resume":
32866
34974
  resume = true;
@@ -32959,7 +35067,7 @@ async function resolveGroundTruthLaunch(mode, cwd) {
32959
35067
  }
32960
35068
  for (const entry of comparisons) {
32961
35069
  for (const [role, p] of [["actual", entry.actualPath], ["expected", entry.expectedPath]]) {
32962
- if (!existsSync20(p)) {
35070
+ if (!existsSync22(p)) {
32963
35071
  console.error(`Error: ${role} image does not exist: ${p}`);
32964
35072
  process.exit(1);
32965
35073
  }
@@ -33009,17 +35117,17 @@ async function main() {
33009
35117
  console.log("AI service test mode enabled \u2014 using mock AI responses");
33010
35118
  }
33011
35119
  if (debug) {
33012
- console.log(`[debug] Build timestamp: ${"2026-07-24T02:30:22.046Z"}`);
35120
+ console.log(`[debug] Build timestamp: ${"2026-08-03T07:40:54.168Z"}`);
33013
35121
  }
33014
35122
  if (projectDir !== null) {
33015
- if (!existsSync20(projectDir) || !statSync8(projectDir).isDirectory()) {
35123
+ if (!existsSync22(projectDir) || !statSync9(projectDir).isDirectory()) {
33016
35124
  console.error(`--project-dir is not a directory: ${projectDir}`);
33017
35125
  process.exit(1);
33018
35126
  }
33019
35127
  process.chdir(projectDir);
33020
35128
  }
33021
35129
  if (dataDir === null) {
33022
- dataDir = join25(process.cwd(), ".glassbox");
35130
+ dataDir = join32(process.cwd(), ".glassbox");
33023
35131
  }
33024
35132
  if (difftoolServe) {
33025
35133
  await runDifftoolServe(dataDir, port, { noOpen, strictPort, onComplete });
@@ -33035,7 +35143,7 @@ async function main() {
33035
35143
  }
33036
35144
  process.exit(1);
33037
35145
  }
33038
- dataDir = join25(tmpdir2(), `glassbox-demo-${demo}-${Date.now()}`);
35146
+ dataDir = join32(tmpdir4(), `glassbox-demo-${demo}-${Date.now()}`);
33039
35147
  setDemoMode(demo);
33040
35148
  console.log(`
33041
35149
  DEMO MODE: ${scenario.label}
@@ -33066,23 +35174,23 @@ async function main() {
33066
35174
  if (mode.type === "diff") {
33067
35175
  const { pathA, pathB } = mode;
33068
35176
  for (const p of [pathA, pathB]) {
33069
- if (!existsSync20(p)) {
35177
+ if (!existsSync22(p)) {
33070
35178
  console.error(`Error: path does not exist: ${p}`);
33071
35179
  process.exit(1);
33072
35180
  }
33073
35181
  }
33074
- if (statSync8(pathA).isDirectory() !== statSync8(pathB).isDirectory()) {
35182
+ if (statSync9(pathA).isDirectory() !== statSync9(pathB).isDirectory()) {
33075
35183
  console.error("Error: --diff requires two files or two folders, not a mix of both.");
33076
35184
  process.exit(1);
33077
35185
  }
33078
35186
  repoRoot = cwd;
33079
- repoName = `${basename5(pathA)} \u2194 ${basename5(pathB)}`;
35187
+ repoName = `${basename7(pathA)} \u2194 ${basename7(pathB)}`;
33080
35188
  } else if (mode.type === "ground-truth") {
33081
35189
  const resolved = await resolveGroundTruthLaunch(mode, cwd);
33082
35190
  mode = resolved.mode;
33083
35191
  for (const [k, v] of resolved.scores) groundTruthScores.set(k, v);
33084
35192
  repoRoot = cwd;
33085
- repoName = `Ground truth: ${basename5(mode.manifestPath)}`;
35193
+ repoName = `Ground truth: ${basename7(mode.manifestPath)}`;
33086
35194
  } else {
33087
35195
  if (!isGitRepo(cwd)) {
33088
35196
  console.error("Error: Not a git repository. Run this from inside a git repo.");