cortad 0.3.11 → 0.4.1
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/README.md +13 -9
- package/lib/auto-reach.mjs +6 -5
- package/lib/contact-text.mjs +3 -4
- package/lib/env-files.mjs +0 -85
- package/lib/files.mjs +5 -0
- package/lib/fresh.mjs +1 -1
- package/lib/mcp.mjs +3 -3
- package/lib/proc.mjs +3 -1
- package/lib/proxy.mjs +14 -3
- package/lib/pyhook/sitecustomize.py +1 -500
- package/lib/reach.mjs +7 -7
- package/lib/read-text.mjs +0 -1
- package/lib/replay.mjs +37 -29
- package/lib/start.mjs +14 -34
- package/lib/switches.mjs +9 -69
- package/lib/trace.cjs +2 -320
- package/local.mjs +168 -518
- package/package.json +2 -17
- package/skill/SKILL.md +12 -10
- package/skill/references/results.md +5 -5
- package/lib/backing.mjs +0 -230
- package/lib/container.mjs +0 -60
- package/lib/data.mjs +0 -143
- package/lib/elastic.mjs +0 -88
- package/lib/hosted.mjs +0 -201
- package/lib/identities.mjs +0 -689
- package/lib/keeping.mjs +0 -181
- package/lib/ledger.mjs +0 -20
- package/lib/listing.mjs +0 -12
- package/lib/mint.mjs +0 -350
- package/lib/mongo.mjs +0 -135
- package/lib/pg.mjs +0 -118
- package/lib/qdrant.mjs +0 -48
- package/lib/redis.mjs +0 -130
- package/lib/service.mjs +0 -96
- package/lib/stores.mjs +0 -433
package/lib/elastic.mjs
DELETED
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
// An Elasticsearch or OpenSearch server copied for a session into a container of ours
|
|
2
|
-
// (lib/container.mjs): every index the app can see, with its settings, mappings, aliases and
|
|
3
|
-
// documents, read over the server's own HTTP API and written into ours. The app names its indexes in
|
|
4
|
-
// its code, so it is the server that is copied, and the app is pointed at ours through its address.
|
|
5
|
-
import { randomBytes } from "node:crypto";
|
|
6
|
-
import { dockerUp, freePort, runCopy, servedBy, until } from "./container.mjs";
|
|
7
|
-
|
|
8
|
-
// A server holding more than this on its primaries is not copied: held instead, so the person decides.
|
|
9
|
-
export const ELASTIC_CAP = 1024 ** 3;
|
|
10
|
-
const PAGE = 500;
|
|
11
|
-
// Index settings the server makes for itself, or that tie the index to the original's cluster.
|
|
12
|
-
const OWN = new Set(["uuid", "creation_date", "creation_date_string", "version", "provided_name", "routing", "resize", "history", "verified_before_close", "blocks", "lifecycle", "replication", "frozen", "store", "shrink", "downsample"]);
|
|
13
|
-
const IMAGE = { elasticsearch: (v) => `docker.elastic.co/elasticsearch/elasticsearch:${v}`, opensearch: (v) => `opensearchproject/opensearch:${v}` };
|
|
14
|
-
// Each kind as one node that asks for no sign-in.
|
|
15
|
-
const SETTINGS = {
|
|
16
|
-
elasticsearch: { "xpack.security.enabled": "false", ES_JAVA_OPTS: "-Xms512m -Xmx512m" },
|
|
17
|
-
opensearch: { DISABLE_SECURITY_PLUGIN: "true", DISABLE_INSTALL_DEMO_CONFIG: "true", OPENSEARCH_JAVA_OPTS: "-Xms512m -Xmx512m" },
|
|
18
|
-
};
|
|
19
|
-
// A server the app signs in to is copied into one that asks for a password too, over plain HTTP on
|
|
20
|
-
// this machine's loopback.
|
|
21
|
-
const SECURED = { "xpack.security.enabled": "true", "xpack.security.http.ssl.enabled": "false", "xpack.security.transport.ssl.enabled": "false" };
|
|
22
|
-
|
|
23
|
-
const headersOf = (conn) => ({
|
|
24
|
-
"content-type": "application/json",
|
|
25
|
-
...(conn.apiKey ? { authorization: `ApiKey ${conn.apiKey}` } : conn.user ? { authorization: `Basic ${Buffer.from(`${conn.user}:${conn.password ?? ""}`).toString("base64")}` } : {}),
|
|
26
|
-
});
|
|
27
|
-
async function call(conn, method, path, body, type) {
|
|
28
|
-
const res = await fetch(`${conn.base}${path}`, {
|
|
29
|
-
method,
|
|
30
|
-
headers: { ...headersOf(conn), ...(type ? { "content-type": type } : {}) },
|
|
31
|
-
body: body === undefined ? undefined : typeof body === "string" ? body : JSON.stringify(body),
|
|
32
|
-
signal: AbortSignal.timeout(60_000),
|
|
33
|
-
});
|
|
34
|
-
const data = await res.json().catch(() => null);
|
|
35
|
-
if (!res.ok) throw Object.assign(new Error(String(res.status)), { why: res.status === 401 || res.status === 403 ? "auth" : "other" });
|
|
36
|
-
return data;
|
|
37
|
-
}
|
|
38
|
-
const whyOf = (e) => e?.why ?? (/ECONNREFUSED|ECONNRESET|fetch failed|timeout/i.test(String(e?.cause?.code ?? e?.message)) ? "down" : "other");
|
|
39
|
-
const at = (index) => `/${encodeURIComponent(index)}`;
|
|
40
|
-
|
|
41
|
-
const ownSettings = (index) => Object.fromEntries(Object.entries(index ?? {}).filter(([k]) => !OWN.has(k)).map(([k, v]) => [k, k === "number_of_replicas" ? "0" : v]));
|
|
42
|
-
|
|
43
|
-
// One index: made on ours as the original holds it, then its documents a page at a time.
|
|
44
|
-
async function copyIndex(from, to, index) {
|
|
45
|
-
const held = (await call(from, "GET", at(index)))[index];
|
|
46
|
-
await call(to, "PUT", at(index), { settings: { index: ownSettings(held.settings?.index) }, mappings: held.mappings ?? {}, aliases: held.aliases ?? {} });
|
|
47
|
-
let page = await call(from, "POST", `${at(index)}/_search?scroll=2m`, { size: PAGE, sort: ["_doc"] });
|
|
48
|
-
try {
|
|
49
|
-
while (page.hits.hits.length) {
|
|
50
|
-
const lines = page.hits.hits.flatMap((h) => [JSON.stringify({ index: { _index: index, _id: h._id, ...(h._routing ? { routing: h._routing } : {}) } }), JSON.stringify(h._source ?? {})]);
|
|
51
|
-
const done = await call(to, "POST", "/_bulk", `${lines.join("\n")}\n`, "application/x-ndjson");
|
|
52
|
-
if (done.errors) throw Object.assign(new Error("bulk"), { why: "other" });
|
|
53
|
-
page = await call(from, "POST", "/_search/scroll", { scroll: "2m", scroll_id: page._scroll_id });
|
|
54
|
-
}
|
|
55
|
-
} finally {
|
|
56
|
-
await call(from, "DELETE", "/_search/scroll", { scroll_id: page._scroll_id }).catch(() => {});
|
|
57
|
-
}
|
|
58
|
-
await call(to, "POST", `${at(index)}/_refresh`);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// Answers { port } or { why }. `conn`: base and port, and user and password or apiKey when the server asks.
|
|
62
|
-
export async function serveElastic(conn, name) {
|
|
63
|
-
let info, indexes;
|
|
64
|
-
try {
|
|
65
|
-
info = await call(conn, "GET", "/");
|
|
66
|
-
indexes = (await call(conn, "GET", "/_cat/indices?format=json&bytes=b&h=index,pri.store.size&expand_wildcards=open")).filter((i) => !i.index.startsWith("."));
|
|
67
|
-
} catch (e) { return { why: whyOf(e) }; }
|
|
68
|
-
if (indexes.reduce((n, i) => n + Number(i["pri.store.size"] ?? 0), 0) > ELASTIC_CAP) return { why: "big" };
|
|
69
|
-
const kind = info.version?.distribution === "opensearch" ? "opensearch" : "elasticsearch";
|
|
70
|
-
// An API key cannot be made again on another server, and OpenSearch keeps its users in a plugin.
|
|
71
|
-
if (conn.apiKey || (conn.user && kind === "opensearch")) return { why: "login" };
|
|
72
|
-
if (!(await dockerUp())) return { why: "docker" };
|
|
73
|
-
const image = (await servedBy(conn.port))?.image ?? IMAGE[kind](info.version?.number);
|
|
74
|
-
const ours = await freePort();
|
|
75
|
-
// The app's own user and password work on ours: the superuser's where it signs in as that, else
|
|
76
|
-
// a user of the same name made there.
|
|
77
|
-
const elastic = conn.user ? (conn.user === "elastic" ? conn.password : randomBytes(24).toString("hex")) : null;
|
|
78
|
-
// No shard is left unplaced on a laptop whose disk is nearly full.
|
|
79
|
-
const env = { ...SETTINGS[kind], ...(elastic ? SECURED : {}), "discovery.type": "single-node", "http.port": String(ours), "cluster.routing.allocation.disk.threshold_enabled": "false" };
|
|
80
|
-
if (!(await runCopy({ name, image, port: ours, env, secrets: elastic ? { ELASTIC_PASSWORD: elastic } : {} }))) return { why: "container" };
|
|
81
|
-
const to = { base: `http://127.0.0.1:${ours}`, ...(elastic ? { user: "elastic", password: elastic } : {}) };
|
|
82
|
-
if (!(await until(async () => Boolean(await call(to, "GET", "/_cluster/health?wait_for_status=yellow&timeout=1s")), 180_000))) return { why: "other" };
|
|
83
|
-
try {
|
|
84
|
-
if (elastic && conn.user !== "elastic") await call(to, "PUT", `/_security/user/${encodeURIComponent(conn.user)}`, { password: conn.password, roles: ["superuser"] });
|
|
85
|
-
for (const { index } of indexes) await copyIndex(conn, to, index);
|
|
86
|
-
} catch (e) { return { why: whyOf(e) }; }
|
|
87
|
-
return { port: ours };
|
|
88
|
-
}
|
package/lib/hosted.mjs
DELETED
|
@@ -1,201 +0,0 @@
|
|
|
1
|
-
// A store the app's settings name on a server off this machine. Trials write for real, so a run
|
|
2
|
-
// never writes into one without the person's yes: the app is pointed at a copy made where the
|
|
3
|
-
// store's maker makes one (a Neon branch, a local Supabase started from the repository's own
|
|
4
|
-
// config), or at an empty database on this machine built by the app's own migrations. keep()
|
|
5
|
-
// answers { env, pass, how } or { why } with every reason no copy could be made, most useful first.
|
|
6
|
-
// Nothing here copies a row out of the hosted store.
|
|
7
|
-
import { spawn } from "node:child_process";
|
|
8
|
-
import { existsSync, readdirSync } from "node:fs";
|
|
9
|
-
import { userInfo } from "node:os";
|
|
10
|
-
import { join } from "node:path";
|
|
11
|
-
import { applySql, dropPg, emptyPg } from "./pg.mjs";
|
|
12
|
-
|
|
13
|
-
const NEON_API = "https://console.neon.tech/api/v2";
|
|
14
|
-
const NEON_HOST = /\.neon\.tech$/i;
|
|
15
|
-
// The endpoint a Neon host serves, pooled or not: ep-cool-darkness-123456(-pooler).region.aws.neon.tech.
|
|
16
|
-
const ENDPOINT = /^(ep-[a-z0-9-]+?)(?:-pooler)?\./i;
|
|
17
|
-
const SUPABASE_HOST = /\.supabase\.(?:co|in|com)$/i;
|
|
18
|
-
const READY_MS = 90_000;
|
|
19
|
-
const MIGRATE_MS = 180_000;
|
|
20
|
-
const PROJECTS_MAX = 100;
|
|
21
|
-
|
|
22
|
-
// A command of the repository's own, with the settings the app starts with. Never a shell.
|
|
23
|
-
export function runCmd(bin, args, { cwd, env = process.env, timeout = 60_000 } = {}) {
|
|
24
|
-
return new Promise((done) => {
|
|
25
|
-
let out = "";
|
|
26
|
-
const child = spawn(bin, args, { cwd, env, stdio: ["ignore", "pipe", "pipe"] });
|
|
27
|
-
const timer = setTimeout(() => child.kill("SIGKILL"), timeout);
|
|
28
|
-
child.stdout.on("data", (d) => { out = (out + d).slice(-20_000); });
|
|
29
|
-
child.stderr.on("data", () => {});
|
|
30
|
-
child.on("error", () => { clearTimeout(timer); done({ ok: false, out }); });
|
|
31
|
-
child.on("close", (code) => { clearTimeout(timer); done({ ok: code === 0, out }); });
|
|
32
|
-
});
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
const URL_SETTING = /^[a-z][\w+.-]*:\/\//i;
|
|
36
|
-
const settingWith = (values, text) => Object.keys(values).filter((k) => String(values[k] ?? "").includes(text));
|
|
37
|
-
const swapped = (values, names, from, to) => Object.fromEntries(names.map((k) => [k, String(values[k]).split(from).join(to)]));
|
|
38
|
-
|
|
39
|
-
function neonApi(key, fetchFn) {
|
|
40
|
-
return async (method, path, body) => {
|
|
41
|
-
const res = await fetchFn(`${NEON_API}${path}`, {
|
|
42
|
-
method,
|
|
43
|
-
headers: { authorization: `Bearer ${key}`, accept: "application/json", ...(body ? { "content-type": "application/json" } : {}) },
|
|
44
|
-
...(body ? { body: JSON.stringify(body) } : {}),
|
|
45
|
-
signal: AbortSignal.timeout(20_000),
|
|
46
|
-
});
|
|
47
|
-
if (!res.ok) throw new Error(`neon answered ${res.status}`);
|
|
48
|
-
return res.status === 204 ? {} : res.json();
|
|
49
|
-
};
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// The project and branch behind an endpoint: the API has no lookup by endpoint, so each project is asked.
|
|
53
|
-
async function neonParent(api, endpoint) {
|
|
54
|
-
let cursor = "";
|
|
55
|
-
for (let seen = 0; seen < PROJECTS_MAX;) {
|
|
56
|
-
const page = await api("GET", `/projects?limit=100${cursor ? `&cursor=${encodeURIComponent(cursor)}` : ""}`);
|
|
57
|
-
const projects = page.projects ?? [];
|
|
58
|
-
for (const p of projects) {
|
|
59
|
-
const { endpoints = [] } = await api("GET", `/projects/${encodeURIComponent(p.id)}/endpoints`);
|
|
60
|
-
const at = endpoints.find((e) => e.id === endpoint);
|
|
61
|
-
if (at) return { project: p.id, branch: at.branch_id };
|
|
62
|
-
}
|
|
63
|
-
seen += projects.length;
|
|
64
|
-
cursor = page.pagination?.cursor ?? "";
|
|
65
|
-
if (!projects.length || !cursor) break;
|
|
66
|
-
}
|
|
67
|
-
return null;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
async function settled(api, project, branch) {
|
|
71
|
-
for (const until = Date.now() + READY_MS; Date.now() < until; await new Promise((r) => setTimeout(r, 1000))) {
|
|
72
|
-
const got = await api("GET", `/projects/${project}/branches/${branch}`);
|
|
73
|
-
if (got.branch?.current_state === "ready") return true;
|
|
74
|
-
}
|
|
75
|
-
return false;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// How the app's schema is built where it has none yet, by the tool the repository already uses.
|
|
79
|
-
const venvBin = (dir, bin) => ["venv", ".venv"].map((v) => join(dir, v, "bin", bin)).find(existsSync) ?? bin;
|
|
80
|
-
const MIGRATORS = [
|
|
81
|
-
{ tool: "Prisma", finds: (d) => existsSync(join(d, "prisma", "schema.prisma")),
|
|
82
|
-
cmd: (d) => ["npx", existsSync(join(d, "prisma", "migrations")) ? ["--no-install", "prisma", "migrate", "deploy"] : ["--no-install", "prisma", "db", "push", "--skip-generate"]] },
|
|
83
|
-
{ tool: "Drizzle", finds: (d) => ["ts", "js", "mjs", "cjs", "json"].some((x) => existsSync(join(d, `drizzle.config.${x}`))), cmd: () => ["npx", ["--no-install", "drizzle-kit", "push"]] },
|
|
84
|
-
{ tool: "Alembic", finds: (d) => existsSync(join(d, "alembic.ini")), cmd: (d) => [venvBin(d, "alembic"), ["upgrade", "head"]] },
|
|
85
|
-
{ tool: "Django", finds: (d) => existsSync(join(d, "manage.py")), cmd: (d) => [venvBin(d, "python3"), ["manage.py", "migrate", "--noinput"]] },
|
|
86
|
-
{ tool: "Supabase", finds: (d) => existsSync(join(d, "supabase", "migrations")), files: (d) => readdirSync(join(d, "supabase", "migrations")).filter((f) => f.endsWith(".sql")).sort().map((f) => join(d, "supabase", "migrations", f)) },
|
|
87
|
-
];
|
|
88
|
-
|
|
89
|
-
// `values`: every setting the app starts with. `dirs`: the app's folder, then the repository's.
|
|
90
|
-
// `local`: the Postgres on this machine a stand-in is made on. `book`: the ledger, so what a session
|
|
91
|
-
// that died made is undone by the next one.
|
|
92
|
-
export function hostedKeeper({ values, dirs, book, fetchFn = globalThis.fetch, run = runCmd, local = null }) {
|
|
93
|
-
const made = [];
|
|
94
|
-
const neonKey = String(values.NEON_API_KEY ?? "").trim();
|
|
95
|
-
const api = neonKey ? neonApi(neonKey, fetchFn) : null;
|
|
96
|
-
const here = local ?? { host: "localhost", port: 5432, user: process.env.PGUSER || userInfo().username, password: process.env.PGPASSWORD ?? "" };
|
|
97
|
-
let supabase = null;
|
|
98
|
-
// Only a setting that is a whole address can be pointed at a copy: one naming a part of the store
|
|
99
|
-
// would keep pointing at the hosted one.
|
|
100
|
-
const inParts = (s) => !s.names.length || !s.names.every((n) => URL_SETTING.test(String(values[n] ?? "")));
|
|
101
|
-
|
|
102
|
-
const undo = {
|
|
103
|
-
branch: async (row) => { if (!api) return false; await api("DELETE", `/projects/${row.made}`); return true; },
|
|
104
|
-
shadow: (row) => dropPg(here, row.made),
|
|
105
|
-
// Their volumes are kept: a local Supabase they stopped themselves comes back with its data.
|
|
106
|
-
supabase: async (row) => (await run("supabase", ["stop"], { cwd: row.made, timeout: 120_000 })).ok,
|
|
107
|
-
};
|
|
108
|
-
const undone = async (row) => { if (await undo[row.kind](row).catch(() => false)) book.remove(row); };
|
|
109
|
-
|
|
110
|
-
async function neon(s) {
|
|
111
|
-
const endpoint = ENDPOINT.exec(s.host)?.[1];
|
|
112
|
-
const parent = endpoint && (await neonParent(api, endpoint));
|
|
113
|
-
if (!parent) return null;
|
|
114
|
-
const got = await api("POST", `/projects/${parent.project}/branches`, { branch: { parent_id: parent.branch, name: `cortad-${process.pid}-${Date.now()}` }, endpoints: [{ type: "read_write" }] });
|
|
115
|
-
const branch = got.branch?.id;
|
|
116
|
-
const host = got.endpoints?.[0]?.host ?? "";
|
|
117
|
-
const fresh = ENDPOINT.exec(host)?.[1];
|
|
118
|
-
if (!branch) return null;
|
|
119
|
-
const row = { kind: "branch", key: s.key, made: `${parent.project}/branches/${branch}` };
|
|
120
|
-
book.add(row);
|
|
121
|
-
made.push(row);
|
|
122
|
-
if (!fresh || !(await settled(api, parent.project, branch))) return null;
|
|
123
|
-
// Every setting naming the endpoint, pooled or direct, names the branch's: roles and passwords come with it.
|
|
124
|
-
return { how: "neon", env: swapped(values, settingWith(values, endpoint), endpoint, fresh), pass: [host, host.replace(fresh, `${fresh}-pooler`)] };
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
// One local Supabase per session, whichever store asks first; stopped at the end only when started here.
|
|
128
|
-
async function localSupabase() {
|
|
129
|
-
const dir = dirs.find((d) => existsSync(join(d, "supabase", "config.toml")));
|
|
130
|
-
if (!dir || !(await run("supabase", ["--version"], { timeout: 15_000 })).ok || !(await run("docker", ["info"], { timeout: 15_000 })).ok) return null;
|
|
131
|
-
let status = await run("supabase", ["status", "-o", "env"], { cwd: dir, timeout: 30_000 });
|
|
132
|
-
let started = false;
|
|
133
|
-
if (!status.ok) {
|
|
134
|
-
const row = { kind: "supabase", key: dir, made: dir };
|
|
135
|
-
book.add(row);
|
|
136
|
-
made.push(row);
|
|
137
|
-
started = true;
|
|
138
|
-
if (!(await run("supabase", ["start"], { cwd: dir, timeout: 600_000 })).ok) return null;
|
|
139
|
-
status = await run("supabase", ["status", "-o", "env"], { cwd: dir, timeout: 30_000 });
|
|
140
|
-
if (!status.ok) return null;
|
|
141
|
-
}
|
|
142
|
-
const vars = Object.fromEntries(status.out.split("\n").map((l) => /^([A-Z_]+)="?([^"]*)"?$/.exec(l.trim())).filter(Boolean).map((m) => [m[1], m[2]]));
|
|
143
|
-
return vars.API_URL ? { vars, started } : null;
|
|
144
|
-
}
|
|
145
|
-
async function supabaseCopy(s) {
|
|
146
|
-
const got = await (supabase ??= localSupabase().catch(() => null));
|
|
147
|
-
if (!got) return null;
|
|
148
|
-
const { vars } = got;
|
|
149
|
-
const env = {};
|
|
150
|
-
if (s.engine === "postgres") for (const n of s.names) env[n] = vars.DB_URL;
|
|
151
|
-
if (s.engine === "supabase") {
|
|
152
|
-
for (const n of s.names) env[n] = vars.API_URL;
|
|
153
|
-
for (const k of Object.keys(values).filter((k) => /SUPABASE/i.test(k))) {
|
|
154
|
-
const v = /ANON|PUBLISHABLE/i.test(k) ? vars.ANON_KEY ?? vars.PUBLISHABLE_KEY : /SERVICE_ROLE|SECRET_KEY/i.test(k) ? vars.SERVICE_ROLE_KEY ?? vars.SECRET_KEY : /JWT_SECRET/i.test(k) ? vars.JWT_SECRET : undefined;
|
|
155
|
-
if (v) env[k] = v;
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
return Object.values(env).every(Boolean) && Object.keys(env).length ? { how: "supabase", env, pass: [], started: got.started } : null;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
// An empty database on this machine, built by the app's own migrations.
|
|
162
|
-
async function shadow(s) {
|
|
163
|
-
if (inParts(s)) return { why: "parts" };
|
|
164
|
-
const dir = dirs.find((d) => MIGRATORS.some((m) => m.finds(d)));
|
|
165
|
-
const migrator = dir && MIGRATORS.find((m) => m.finds(dir));
|
|
166
|
-
if (!migrator) return { why: "no-migrations" };
|
|
167
|
-
const name = `cortad_shadow_${process.pid}_${Date.now().toString(36)}`;
|
|
168
|
-
const row = { kind: "shadow", key: s.key, made: name };
|
|
169
|
-
book.add(row);
|
|
170
|
-
const got = await emptyPg(here, name);
|
|
171
|
-
if (got.why) { book.remove(row); return { why: got.why === "down" || got.why === "auth" || got.why === "missing" ? "no-local" : "other" }; }
|
|
172
|
-
made.push(row);
|
|
173
|
-
const address = `${encodeURIComponent(here.user)}${here.password ? `:${encodeURIComponent(here.password)}` : ""}@${here.host}:${here.port}/${name}`;
|
|
174
|
-
const env = Object.fromEntries(s.names.map((n) => [n, `${/^([a-z][\w+.-]*):\/\//i.exec(values[n])[1]}://${address}`]));
|
|
175
|
-
const ok = migrator.files
|
|
176
|
-
? await (async () => { for (const f of migrator.files(dir)) if (!(await applySql(here, name, f))) return false; return true; })()
|
|
177
|
-
: (await run(...migrator.cmd(dir), { cwd: dir, env: { ...process.env, ...values, ...env }, timeout: MIGRATE_MS })).ok;
|
|
178
|
-
return ok ? { how: "shadow", tool: migrator.tool, env, pass: [] } : { why: "migrations", tool: migrator.tool };
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
return {
|
|
182
|
-
// Copies a session that died left behind.
|
|
183
|
-
orphans: async () => { for (const kind of Object.keys(undo)) for (const row of book.orphans(kind)) await undone(row); },
|
|
184
|
-
async keep(s) {
|
|
185
|
-
const why = [];
|
|
186
|
-
if (NEON_HOST.test(s.host)) {
|
|
187
|
-
if (!api) why.push("neon-key");
|
|
188
|
-
else { const got = await neon(s).catch(() => null); if (got) return got; why.push("neon"); }
|
|
189
|
-
}
|
|
190
|
-
if (SUPABASE_HOST.test(s.host) || s.engine === "supabase") {
|
|
191
|
-
const got = inParts(s) ? null : await supabaseCopy(s).catch(() => null);
|
|
192
|
-
if (got) return got;
|
|
193
|
-
why.push(inParts(s) ? "parts" : "supabase");
|
|
194
|
-
}
|
|
195
|
-
if (s.engine !== "postgres") return { why: [...why, "engine"] };
|
|
196
|
-
const got = await shadow(s).catch(() => ({ why: "other" }));
|
|
197
|
-
return got.env ? got : { why: [...why, got.why], tool: got.tool };
|
|
198
|
-
},
|
|
199
|
-
drop: async () => { for (const row of made.splice(0).reverse()) await undone(row); },
|
|
200
|
-
};
|
|
201
|
-
}
|