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 +2415 -307
- package/dist/client/app.global.js +45 -45
- package/dist/client/history.global.js +23 -23
- package/dist/client/styles.css +1 -1
- package/package.json +7 -5
package/dist/cli.js
CHANGED
|
@@ -636,14 +636,14 @@ function prefixIssues(path, issues) {
|
|
|
636
636
|
return iss;
|
|
637
637
|
});
|
|
638
638
|
}
|
|
639
|
-
function unwrapMessage(
|
|
640
|
-
return typeof
|
|
639
|
+
function unwrapMessage(message2) {
|
|
640
|
+
return typeof message2 === "string" ? message2 : message2?.message;
|
|
641
641
|
}
|
|
642
642
|
function finalizeIssue(iss, ctx, config2) {
|
|
643
|
-
const
|
|
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 =
|
|
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
|
|
15685
|
-
if (!
|
|
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
|
|
15819
|
-
if (!
|
|
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
|
-
//
|
|
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 {
|
|
16786
|
-
|
|
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 =
|
|
18738
|
+
const dbDir = join7(dataDir, "data");
|
|
16790
18739
|
mkdirSync3(dbDir, { recursive: true });
|
|
16791
|
-
currentDbPath =
|
|
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
|
|
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
|
|
16807
|
-
if (
|
|
16808
|
-
console.error(
|
|
16809
|
-
|
|
16810
|
-
|
|
16811
|
-
|
|
16812
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
18845
|
+
DB_OPTIONS2 = { database: "template1" };
|
|
16860
18846
|
}
|
|
16861
18847
|
});
|
|
16862
18848
|
|
|
16863
18849
|
// src/attachments/store.ts
|
|
16864
|
-
import { createHash as
|
|
16865
|
-
import { existsSync as
|
|
16866
|
-
import { basename as
|
|
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 :
|
|
18855
|
+
return dataDir === null ? null : join8(dataDir, "attachments");
|
|
16870
18856
|
}
|
|
16871
18857
|
function sanitizeFilename(name) {
|
|
16872
|
-
const base =
|
|
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 =
|
|
18866
|
+
const storedPath = join8(dir, `${id}-${sanitizeFilename(originalFilename)}`);
|
|
16881
18867
|
writeFileSync2(storedPath, bytes);
|
|
16882
|
-
const sha256 =
|
|
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 (
|
|
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
|
|
17388
|
-
import { homedir } from "os";
|
|
17389
|
-
import { join as
|
|
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
|
|
19379
|
+
return join9(homedir3(), ".glassbox");
|
|
17394
19380
|
}
|
|
17395
19381
|
function readGlobalConfig() {
|
|
17396
19382
|
try {
|
|
17397
|
-
if (
|
|
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 =
|
|
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
|
|
17566
|
-
import { join as
|
|
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
|
|
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(
|
|
19563
|
+
writeFileSync4(join10(dir, blobName(fileId, side)), bytes);
|
|
17578
19564
|
}
|
|
17579
19565
|
function readImageBlob(dataDir, fileId, side) {
|
|
17580
|
-
const path =
|
|
17581
|
-
if (!
|
|
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
|
|
17684
|
-
import { join as
|
|
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
|
|
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 (
|
|
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 =
|
|
19690
|
+
const dir = join15(repoRoot, ".glassbox");
|
|
17705
19691
|
mkdirSync8(dir, { recursive: true });
|
|
17706
|
-
writeFileSync9(
|
|
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
|
|
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
|
|
18061
|
-
import { join as
|
|
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
|
|
20068
|
+
return join16(GLOBAL_CONFIG_DIR, "plugins");
|
|
18083
20069
|
}
|
|
18084
20070
|
function discoverPluginDirs(root = pluginsDir()) {
|
|
18085
|
-
if (!
|
|
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 =
|
|
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 =
|
|
18105
|
-
if (
|
|
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 =
|
|
18113
|
-
if (
|
|
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,
|
|
18130
|
-
const line = `[plugin:${id}] ${
|
|
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 =
|
|
18159
|
-
if (!
|
|
18160
|
-
const mod = await import(
|
|
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
|
-
|
|
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
|
|
18203
|
-
import { cpSync, existsSync as
|
|
18204
|
-
import { dirname as
|
|
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
|
|
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 =
|
|
18213
|
-
if (
|
|
18214
|
-
return
|
|
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 (!
|
|
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 =
|
|
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(
|
|
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 =
|
|
20236
|
+
const h = createHash5("sha1");
|
|
18251
20237
|
const walk = (d, rel) => {
|
|
18252
20238
|
for (const name of readdirSync3(d).sort()) {
|
|
18253
|
-
const abs =
|
|
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 (!
|
|
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 (!
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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(
|
|
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
|
-
|
|
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
|
|
18741
|
-
import { homedir as
|
|
18742
|
-
import { join as
|
|
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
|
|
20730
|
+
return join31(homedir5(), ".glassbox");
|
|
18745
20731
|
}
|
|
18746
20732
|
function discoveryPath(home = difftoolHome()) {
|
|
18747
|
-
return
|
|
20733
|
+
return join31(home, "difftool.lock");
|
|
18748
20734
|
}
|
|
18749
20735
|
function startingLockPath(home = difftoolHome()) {
|
|
18750
|
-
return
|
|
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 (!
|
|
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
|
-
|
|
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() -
|
|
20775
|
+
const ageMs = Date.now() - statSync8(path).mtimeMs;
|
|
18790
20776
|
if (ageMs > STARTING_LOCK_STALE_MS) {
|
|
18791
|
-
|
|
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
|
-
|
|
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
|
|
20696
|
-
for (var j in
|
|
20697
|
-
|
|
20698
|
-
delete
|
|
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
|
|
23336
|
-
import { tmpdir as
|
|
23337
|
-
import { basename as
|
|
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
|
|
24459
|
-
import { tmpdir } from "os";
|
|
24460
|
-
import { basename as
|
|
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 :
|
|
26677
|
+
return statSync2(p).isDirectory() ? p : dirname7(p);
|
|
24692
26678
|
} catch {
|
|
24693
|
-
return
|
|
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(
|
|
24722
|
-
const rootA =
|
|
24723
|
-
const rootB =
|
|
24724
|
-
const oldAbs =
|
|
24725
|
-
const newAbs =
|
|
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(
|
|
24728
|
-
mkdirSync7(
|
|
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(
|
|
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(
|
|
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 =
|
|
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 =
|
|
24886
|
-
const newAbs =
|
|
24887
|
-
if (!
|
|
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 `${
|
|
26926
|
+
return `${basename4(mode.pathA)} \u2194 ${basename4(mode.pathB)}`;
|
|
24941
26927
|
case "ground-truth":
|
|
24942
|
-
return
|
|
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
|
|
24953
|
-
import { basename as
|
|
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 (
|
|
25004
|
-
const targetDir =
|
|
26989
|
+
if (basename5(glassboxDir) !== ".glassbox") return { changed: false };
|
|
26990
|
+
const targetDir = dirname8(glassboxDir);
|
|
25005
26991
|
if (!isGitRepo(targetDir)) return { changed: false };
|
|
25006
|
-
const gitignorePath =
|
|
25007
|
-
const existing =
|
|
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
|
|
25016
|
-
import { join as
|
|
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 =
|
|
25021
|
-
if (
|
|
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
|
|
25194
|
-
import { Hono as
|
|
25195
|
-
import { dirname as
|
|
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
|
|
25201
|
-
import { dirname as
|
|
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 =
|
|
25210
|
-
const distPath =
|
|
25211
|
-
if (
|
|
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 =
|
|
25215
|
-
if (
|
|
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 =
|
|
27211
|
+
const mcpPath = join14(root, ".mcp.json");
|
|
25226
27212
|
const { command, args } = getChannelServerPath();
|
|
25227
27213
|
let config2 = {};
|
|
25228
|
-
if (
|
|
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 =
|
|
25246
|
-
if (!
|
|
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(
|
|
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(
|
|
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,
|
|
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:
|
|
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((
|
|
26176
|
-
setTimeout(
|
|
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((
|
|
26221
|
-
setTimeout(
|
|
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
|
|
26352
|
-
console.error(`Analysis failed: ${
|
|
26353
|
-
debugLog(`Analysis ${analysisId} failed: ${
|
|
26354
|
-
await updateAnalysisStatus(analysisId, "failed",
|
|
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,
|
|
28130
|
-
return c.json({ error:
|
|
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
|
|
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
|
|
28522
|
-
import { join as
|
|
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 =
|
|
30803
|
+
const exportDir = join18(repoRoot, ".glassbox");
|
|
28818
30804
|
for (const ext of ["md", "json"]) {
|
|
28819
|
-
const archivePath =
|
|
28820
|
-
if (
|
|
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 =
|
|
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 =
|
|
30866
|
+
const archivePath = join18(exportDir, `review-${review.id}.md`);
|
|
28881
30867
|
writeFileSync11(archivePath, content, "utf-8");
|
|
28882
|
-
writeFileSync11(
|
|
30868
|
+
writeFileSync11(join18(exportDir, `review-${review.id}.json`), json2, "utf-8");
|
|
28883
30869
|
if (isCurrent) {
|
|
28884
|
-
const latestPath =
|
|
30870
|
+
const latestPath = join18(exportDir, "latest-review.md");
|
|
28885
30871
|
writeFileSync11(latestPath, content, "utf-8");
|
|
28886
|
-
writeFileSync11(
|
|
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
|
|
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", [
|
|
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
|
|
29165
|
-
if (!
|
|
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/
|
|
31219
|
+
// src/routes/api/db-backups.ts
|
|
29234
31220
|
import { Hono as Hono7 } from "hono";
|
|
29235
|
-
|
|
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
|
|
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 =
|
|
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:
|
|
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 =
|
|
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
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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
|
|
29593
|
-
import { resolve as
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
30011
|
-
import { join as
|
|
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 (
|
|
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
|
|
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
|
|
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 =
|
|
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(
|
|
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 =
|
|
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
|
|
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
|
|
30246
|
-
var projectSettingsRoutes = new
|
|
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
|
|
30264
|
-
import { Hono as
|
|
30265
|
-
import { extname as extname2, relative as relative2, resolve as
|
|
30266
|
-
var reviewNotesRoutes = new
|
|
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 =
|
|
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
|
|
30289
|
-
if (!
|
|
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
|
|
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
|
|
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 =
|
|
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((
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
32570
|
+
import { Hono as Hono15 } from "hono";
|
|
30464
32571
|
init_global_config();
|
|
30465
|
-
var sharePromptRoutes = new
|
|
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
|
|
30505
|
-
var systemRoutes = new
|
|
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
|
|
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
|
|
30535
|
-
import { join as
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
|
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((
|
|
32771
|
+
return new Promise((resolve15) => {
|
|
30664
32772
|
addDifftoolHold(() => {
|
|
30665
|
-
|
|
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
|
|
30680
|
-
import { relative as relative3, resolve as
|
|
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
|
|
31003
|
-
if (
|
|
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
|
|
31722
|
-
import { join as
|
|
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 =
|
|
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 (!
|
|
33856
|
+
if (!existsSync17(THEMES_DIR)) return [];
|
|
31749
33857
|
const themes = [];
|
|
31750
33858
|
try {
|
|
31751
|
-
for (const file2 of
|
|
31752
|
-
const theme = readStoredTheme(
|
|
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 =
|
|
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 =
|
|
31766
|
-
if (
|
|
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 =
|
|
31772
|
-
if (!
|
|
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
|
|
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 =
|
|
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
|
|
32307
|
-
var themeApiRoutes = new
|
|
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((
|
|
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
|
-
|
|
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
|
|
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 =
|
|
32439
|
-
const distDir =
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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 =
|
|
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
|
|
32513
|
-
import { join as
|
|
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 (
|
|
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 =
|
|
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(
|
|
34672
|
+
return updateFile(join29(dir, "SKILL.md"), content);
|
|
32565
34673
|
}
|
|
32566
34674
|
function ensureCursorRules(cwd) {
|
|
32567
|
-
const rulesDir =
|
|
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(
|
|
34687
|
+
return updateFile(join29(rulesDir, "glassbox.mdc"), content);
|
|
32580
34688
|
}
|
|
32581
34689
|
function ensureCopilotPrompts(cwd) {
|
|
32582
|
-
const promptsDir =
|
|
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(
|
|
34701
|
+
return updateFile(join29(promptsDir, "glassbox.prompt.md"), content);
|
|
32594
34702
|
}
|
|
32595
34703
|
function ensureWindsurfRules(cwd) {
|
|
32596
|
-
const rulesDir =
|
|
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(
|
|
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 (
|
|
34721
|
+
if (existsSync19(join29(cwd, ".claude"))) {
|
|
32614
34722
|
if (ensureClaudeSkills(cwd)) platforms.push("Claude Code");
|
|
32615
34723
|
}
|
|
32616
|
-
if (
|
|
34724
|
+
if (existsSync19(join29(cwd, ".cursor"))) {
|
|
32617
34725
|
if (ensureCursorRules(cwd)) platforms.push("Cursor");
|
|
32618
34726
|
}
|
|
32619
|
-
if (
|
|
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 (
|
|
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
|
|
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
|
|
32635
|
-
import { join as
|
|
32636
|
-
var DATA_DIR =
|
|
32637
|
-
var CHECK_FILE =
|
|
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 (
|
|
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((
|
|
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
|
-
|
|
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
|
-
|
|
34781
|
+
resolve15(VersionPayloadSchema2.parse(raw2).version);
|
|
32674
34782
|
} catch {
|
|
32675
|
-
|
|
34783
|
+
resolve15(null);
|
|
32676
34784
|
}
|
|
32677
34785
|
});
|
|
32678
34786
|
});
|
|
32679
34787
|
req.on("error", () => {
|
|
32680
|
-
|
|
34788
|
+
resolve15(null);
|
|
32681
34789
|
});
|
|
32682
34790
|
req.on("timeout", () => {
|
|
32683
34791
|
req.destroy();
|
|
32684
|
-
|
|
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:
|
|
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:
|
|
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 =
|
|
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 (!
|
|
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-
|
|
35120
|
+
console.log(`[debug] Build timestamp: ${"2026-08-03T07:40:54.168Z"}`);
|
|
33013
35121
|
}
|
|
33014
35122
|
if (projectDir !== null) {
|
|
33015
|
-
if (!
|
|
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 =
|
|
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 =
|
|
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 (!
|
|
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 (
|
|
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 = `${
|
|
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: ${
|
|
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.");
|