@bitcobblers/wod-wiki-tools 0.39.2142 → 0.40.2144
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/bin/wod.cjs +736 -2
- package/dist/bin/wod.cjs.map +1 -1
- package/dist/bin/wod.mjs +2 -2
- package/dist/{chunk-SXIU2PMW.mjs → chunk-464KHLHY.mjs} +737 -6
- package/dist/chunk-464KHLHY.mjs.map +1 -0
- package/dist/{chunk-ED2UAD7M.mjs → chunk-UNDCEABI.mjs} +3 -3
- package/dist/chunk-UNDCEABI.mjs.map +1 -0
- package/dist/index.cjs +736 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +59 -1
- package/dist/index.d.ts +59 -1
- package/dist/index.mjs +2 -2
- package/dist/{scaffoldCatalog-FNI45BKF.mjs → scaffoldCatalog-APBMVJES.mjs} +3 -3
- package/dist/{scaffoldCatalog-FNI45BKF.mjs.map → scaffoldCatalog-APBMVJES.mjs.map} +1 -1
- package/dist/web/playground/assets/main-DXU49DmJ.css +2 -0
- package/dist/web/playground/assets/{main-C3qoIfdW.js → main-DpzRbHM9.js} +103 -103
- package/dist/web/playground/assets/{statementBuilderFlow-CRlw4ZL_.js → statementBuilderFlow-B0UjTiSn.js} +2 -2
- package/dist/web/playground/assets/{vendor-CiB7wDhT.js → vendor-Dk-k8gVh.js} +67 -67
- package/dist/web/playground/assets/{vendor-codemirror-D4vSrLkz.js → vendor-codemirror-2KNYvyJk.js} +2 -2
- package/dist/web/playground/index.html +4 -4
- package/dist/web/playground/seed/core/chunks/canvas.f625050a.json +1 -0
- package/dist/web/playground/seed/core/chunks/efforts.a58dbed3.json +1 -0
- package/dist/web/playground/seed/core/chunks/equipment.c45026fc.json +1 -0
- package/dist/web/playground/seed/core/manifest.json +14 -14
- package/dist/web/playground/seed/core/report.html +13 -13
- package/dist/web/server/assets/main-DXU49DmJ.css +2 -0
- package/dist/web/server/assets/{main-BfrJ_Uwt.js → main-imfCx0Nx.js} +73 -72
- package/dist/web/server/assets/{statementBuilderFlow-Bt_NwExn.js → statementBuilderFlow-BI4ttuZP.js} +2 -2
- package/dist/web/server/assets/{vendor-CiB7wDhT.js → vendor-Dk-k8gVh.js} +67 -67
- package/dist/web/server/assets/{vendor-codemirror-D4vSrLkz.js → vendor-codemirror-2KNYvyJk.js} +2 -2
- package/dist/web/server/index.html +4 -4
- package/dist/web/server/seed/core/chunks/canvas.f625050a.json +1 -0
- package/dist/web/server/seed/core/chunks/efforts.a58dbed3.json +1 -0
- package/dist/web/server/seed/core/chunks/equipment.c45026fc.json +1 -0
- package/dist/web/server/seed/core/manifest.json +14 -14
- package/dist/web/server/seed/core/report.html +13 -13
- package/package.json +8 -7
- package/dist/chunk-ED2UAD7M.mjs.map +0 -1
- package/dist/chunk-SXIU2PMW.mjs.map +0 -1
- package/dist/web/playground/assets/main-C5dNwq9b.css +0 -2
- package/dist/web/playground/seed/core/chunks/canvas.5e48109f.json +0 -1
- package/dist/web/playground/seed/core/chunks/efforts.bf1c24fe.json +0 -1
- package/dist/web/playground/seed/core/chunks/equipment.c4fb1d64.json +0 -1
- package/dist/web/server/assets/main-C5dNwq9b.css +0 -2
- package/dist/web/server/seed/core/chunks/canvas.5e48109f.json +0 -1
- package/dist/web/server/seed/core/chunks/efforts.bf1c24fe.json +0 -1
- package/dist/web/server/seed/core/chunks/equipment.c4fb1d64.json +0 -1
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { createRequire } from 'module';
|
|
2
2
|
import { compileSeedRow, SEED_SCHEMA, writeBuildReport } from './chunk-IEW74LUO.mjs';
|
|
3
|
-
import { VERSION } from './chunk-
|
|
3
|
+
import { VERSION } from './chunk-UNDCEABI.mjs';
|
|
4
4
|
import { openCliDb, nodeFs, sqliteIndexStore, sqliteEffortStore, sqliteBlockStore, sqliteNoteStore, sqliteEventStore, getNoteBundle, getSessionBundle, getEffortBySlug, getEquipmentBySlug, listTagsByType, nodePath, loadSql, uuid5, CLI_UUID_NAMESPACE, nodeCrypto, stableNoteId, randomId } from './chunk-IWT7U3WX.mjs';
|
|
5
5
|
import { __require } from './chunk-6L5ZDZJ7.mjs';
|
|
6
6
|
import { createParser, frozenNow, createCompiler, createMockClock, RuntimeStack, EventBus, ScriptRuntime, createAnalyticsEngineForBlock, StartSessionAction, NextEvent, toStoredOutputStatement, bundledEfforts, parseEffortFile, parseEquipmentFile, createReferenceIndex, parseScript, extractStatementRefs } from '@bitcobblers/wod-wiki-lang';
|
|
7
7
|
import { buildStatementTree, createIRFile, registerLanguagePack, extractNoteIdentity, extractTypedTags, composeNoteMarkdown, sessionToCsv, effortToMarkdown, equipmentToMarkdown, equipmentToCsv, csvToEquipmentLabels, csvToSessionRows, isIRFile, inMemoryEventStore, readFrontmatterMeta, assertSeedManifest, seedMetaKey, SEED_SCHEMA as SEED_SCHEMA$1, emptySeedMeta, CANVAS_CHUNK_ID, assertSeedRows, seedNoteId, applySeedChunk } from '@bitcobblers/wod-wiki-engine';
|
|
8
8
|
import { toEventRows, toSummaryEventRows, parseQuery, QueryService, isPipelineQuery, isFindQuery, catalogOfItem } from '@bitcobblers/wod-wiki-wql';
|
|
9
9
|
import { validateNoteBlocks, parseDocumentSections, blockContentId, reconcileNoteBlocks, KNOWN_SECTION_WIDGETS, MetricType } from '@bitcobblers/wod-wiki-core';
|
|
10
|
-
import { rowsEqual } from '@bitcobblers/wod-wiki-storage';
|
|
10
|
+
import { rowsEqual, DB_VERSION } from '@bitcobblers/wod-wiki-storage';
|
|
11
11
|
|
|
12
12
|
if (typeof globalThis.require === 'undefined') {
|
|
13
13
|
try { globalThis.require = createRequire(import.meta.url); } catch {}
|
|
@@ -923,6 +923,658 @@ ${escapeCsvField(data.parsed.metric)},${data.scalar},${escapeCsvField(data.unit
|
|
|
923
923
|
}
|
|
924
924
|
return lines.join("\n");
|
|
925
925
|
}
|
|
926
|
+
var lifecycleSql;
|
|
927
|
+
function loadSqlLifecycle() {
|
|
928
|
+
lifecycleSql ??= import(
|
|
929
|
+
/* @vite-ignore */
|
|
930
|
+
'@bitcobblers/wod-wiki-storage/sql'
|
|
931
|
+
);
|
|
932
|
+
return lifecycleSql;
|
|
933
|
+
}
|
|
934
|
+
function fullFs() {
|
|
935
|
+
return nodeFs();
|
|
936
|
+
}
|
|
937
|
+
function parseEnvContent(content) {
|
|
938
|
+
const g = globalThis;
|
|
939
|
+
const req = g.require ?? (typeof __require === "function" ? __require : void 0);
|
|
940
|
+
if (!req) throw new Error("[wod db] .env handling requires a CJS require shim (missing tsup banner?)");
|
|
941
|
+
return req("node:util").parseEnv(content);
|
|
942
|
+
}
|
|
943
|
+
function maskSecrets(message, ...secrets) {
|
|
944
|
+
let masked = message;
|
|
945
|
+
for (const secret of secrets) {
|
|
946
|
+
if (secret) masked = masked.split(secret).join("\u2022\u2022\u2022\u2022\u2022\u2022");
|
|
947
|
+
}
|
|
948
|
+
return masked;
|
|
949
|
+
}
|
|
950
|
+
var TursoManagement = class _TursoManagement {
|
|
951
|
+
constructor(organization, token, deps) {
|
|
952
|
+
this.organization = organization;
|
|
953
|
+
this.token = token;
|
|
954
|
+
this.deps = deps;
|
|
955
|
+
}
|
|
956
|
+
organization;
|
|
957
|
+
token;
|
|
958
|
+
deps;
|
|
959
|
+
/** Env-only credentials — never flags, never .env-file-only names. */
|
|
960
|
+
static fromEnv(env, deps = {}) {
|
|
961
|
+
const read = (key) => {
|
|
962
|
+
const value = env[key];
|
|
963
|
+
return value !== void 0 && value.trim() !== "" ? value.trim() : void 0;
|
|
964
|
+
};
|
|
965
|
+
const token = read("TURSO_API_TOKEN");
|
|
966
|
+
const organization = read("TURSO_ORGANIZATION");
|
|
967
|
+
if (!token || !organization) {
|
|
968
|
+
throw new Error(
|
|
969
|
+
"[wod db] management credentials required: set TURSO_API_TOKEN and TURSO_ORGANIZATION in the environment (or .env). Management tokens are never accepted as flags and never printed."
|
|
970
|
+
);
|
|
971
|
+
}
|
|
972
|
+
return new _TursoManagement(organization, token, deps);
|
|
973
|
+
}
|
|
974
|
+
async request(op, method, path, body) {
|
|
975
|
+
const base = this.deps.apiBase ?? "https://api.turso.tech";
|
|
976
|
+
let response;
|
|
977
|
+
try {
|
|
978
|
+
response = await (this.deps.fetchFn ?? fetch)(`${base}${path}`, {
|
|
979
|
+
method,
|
|
980
|
+
headers: { authorization: `Bearer ${this.token}`, ...body !== void 0 ? { "content-type": "application/json" } : {} },
|
|
981
|
+
body: body !== void 0 ? JSON.stringify(body) : void 0,
|
|
982
|
+
signal: AbortSignal.timeout(2e4)
|
|
983
|
+
});
|
|
984
|
+
} catch (error) {
|
|
985
|
+
throw new Error(`[wod db] ${op}: ${maskSecrets(error instanceof Error ? error.message : String(error), this.token)}`, { cause: error });
|
|
986
|
+
}
|
|
987
|
+
if (!response.ok) {
|
|
988
|
+
let detail = "";
|
|
989
|
+
try {
|
|
990
|
+
const parsed = await response.json();
|
|
991
|
+
if (typeof parsed === "object" && parsed !== null && "error" in parsed && typeof parsed.error === "string") {
|
|
992
|
+
detail = parsed.error;
|
|
993
|
+
}
|
|
994
|
+
} catch {
|
|
995
|
+
}
|
|
996
|
+
throw new Error(
|
|
997
|
+
`[wod db] ${op}: Turso API ${response.status} ${maskSecrets(detail || response.statusText, this.token)}`
|
|
998
|
+
);
|
|
999
|
+
}
|
|
1000
|
+
if (response.status === 204) return void 0;
|
|
1001
|
+
const text = await response.text();
|
|
1002
|
+
if (text.trim() === "") return void 0;
|
|
1003
|
+
try {
|
|
1004
|
+
return JSON.parse(text);
|
|
1005
|
+
} catch {
|
|
1006
|
+
throw new Error(`[wod db] ${op}: Turso API returned a non-JSON body with status ${response.status}`);
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
async getDatabase(name) {
|
|
1010
|
+
try {
|
|
1011
|
+
const payload = await this.request(
|
|
1012
|
+
`get database ${name}`,
|
|
1013
|
+
"GET",
|
|
1014
|
+
`/v1/organizations/${encodeURIComponent(this.organization)}/databases/${encodeURIComponent(name)}`
|
|
1015
|
+
);
|
|
1016
|
+
if (typeof payload !== "object" || payload === null || !("database" in payload)) {
|
|
1017
|
+
throw new Error(`[wod db] get database ${name}: Turso API payload has no database field`);
|
|
1018
|
+
}
|
|
1019
|
+
return toDatabaseInfo(payload.database);
|
|
1020
|
+
} catch (error) {
|
|
1021
|
+
if (error instanceof Error && error.message.includes("Turso API 404")) return void 0;
|
|
1022
|
+
throw error;
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
async createDatabase(input) {
|
|
1026
|
+
const payload = await this.request(
|
|
1027
|
+
`create database ${input.name}`,
|
|
1028
|
+
"POST",
|
|
1029
|
+
`/v1/organizations/${encodeURIComponent(this.organization)}/databases`,
|
|
1030
|
+
input
|
|
1031
|
+
);
|
|
1032
|
+
if (typeof payload !== "object" || payload === null || !("database" in payload)) {
|
|
1033
|
+
throw new Error(`[wod db] create database ${input.name}: Turso API payload has no database field`);
|
|
1034
|
+
}
|
|
1035
|
+
return toDatabaseInfo(payload.database);
|
|
1036
|
+
}
|
|
1037
|
+
/** Mint a database-scoped token (spec: authorization full-access|read-only). */
|
|
1038
|
+
async createToken(name, authorization, expiration) {
|
|
1039
|
+
const payload = await this.request(
|
|
1040
|
+
`mint token for ${name}`,
|
|
1041
|
+
"POST",
|
|
1042
|
+
`/v1/organizations/${encodeURIComponent(this.organization)}/databases/${encodeURIComponent(name)}/auth/tokens?authorization=${authorization}&expiration=${encodeURIComponent(expiration)}`
|
|
1043
|
+
);
|
|
1044
|
+
if (typeof payload !== "object" || payload === null || !("jwt" in payload) || typeof payload.jwt !== "string" || payload.jwt === "") {
|
|
1045
|
+
throw new Error(`[wod db] mint token for ${name}: Turso API returned no token`);
|
|
1046
|
+
}
|
|
1047
|
+
return payload.jwt;
|
|
1048
|
+
}
|
|
1049
|
+
async deleteDatabase(name) {
|
|
1050
|
+
await this.request(
|
|
1051
|
+
`delete database ${name}`,
|
|
1052
|
+
"DELETE",
|
|
1053
|
+
`/v1/organizations/${encodeURIComponent(this.organization)}/databases/${encodeURIComponent(name)}`
|
|
1054
|
+
);
|
|
1055
|
+
}
|
|
1056
|
+
};
|
|
1057
|
+
function toDatabaseInfo(value) {
|
|
1058
|
+
if (typeof value !== "object" || value === null || !("Name" in value) || !("Hostname" in value)) {
|
|
1059
|
+
throw new Error("[wod db] Turso API database payload is missing Name/Hostname");
|
|
1060
|
+
}
|
|
1061
|
+
const name = value.Name;
|
|
1062
|
+
const hostname = value.Hostname;
|
|
1063
|
+
if (typeof name !== "string" || name === "" || typeof hostname !== "string" || hostname === "") {
|
|
1064
|
+
throw new Error("[wod db] Turso API database payload is missing Name/Hostname");
|
|
1065
|
+
}
|
|
1066
|
+
return {
|
|
1067
|
+
name,
|
|
1068
|
+
hostname,
|
|
1069
|
+
dbId: "DbId" in value && typeof value.DbId === "string" ? value.DbId : void 0,
|
|
1070
|
+
group: "group" in value && typeof value.group === "string" ? value.group : void 0
|
|
1071
|
+
};
|
|
1072
|
+
}
|
|
1073
|
+
function writeSecretFile(fs, path, content) {
|
|
1074
|
+
const tmp = `${path}.${nodeCrypto().randomUUID()}.tmp`;
|
|
1075
|
+
const fd = fs.openSync(tmp, "wx", 384);
|
|
1076
|
+
try {
|
|
1077
|
+
fs.writeSync(fd, content);
|
|
1078
|
+
fs.closeSync(fd);
|
|
1079
|
+
fs.renameSync(tmp, path);
|
|
1080
|
+
} catch (error) {
|
|
1081
|
+
try {
|
|
1082
|
+
fs.closeSync(fd);
|
|
1083
|
+
} catch {
|
|
1084
|
+
}
|
|
1085
|
+
try {
|
|
1086
|
+
fs.unlinkSync(tmp);
|
|
1087
|
+
} catch {
|
|
1088
|
+
}
|
|
1089
|
+
throw error;
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
var CONTROL_CHARS = /[\u0000-\u001f\u007f\u0080-\u009f]/;
|
|
1093
|
+
function assertSafePairingFields(pairing) {
|
|
1094
|
+
for (const [what, value] of [["target URL", pairing.url], ["auth token", pairing.authToken], ["database name", pairing.database]]) {
|
|
1095
|
+
if (CONTROL_CHARS.test(value)) {
|
|
1096
|
+
throw new Error(`[wod db] refusing: ${what} contains control characters (hostile API response)`);
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
let parsed;
|
|
1100
|
+
try {
|
|
1101
|
+
parsed = new URL(pairing.url);
|
|
1102
|
+
} catch {
|
|
1103
|
+
throw new Error("[wod db] refusing: target URL is not parseable");
|
|
1104
|
+
}
|
|
1105
|
+
if (parsed.protocol !== "libsql:" || parsed.hostname === "" || parsed.search !== "" || parsed.hash !== "") {
|
|
1106
|
+
throw new Error("[wod db] refusing: target URL must be a plain libsql://<hostname>");
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
function writePairingFile(path, pairing, opts = {}) {
|
|
1110
|
+
const fs = fullFs();
|
|
1111
|
+
if (fs.existsSync(path) && !opts.force) {
|
|
1112
|
+
throw new Error(`[wod db] refusing to overwrite existing pairing file ${path} (it contains a credential) \u2014 pass --force to replace it`);
|
|
1113
|
+
}
|
|
1114
|
+
assertSafePairingFields(pairing);
|
|
1115
|
+
writeSecretFile(fs, path, JSON.stringify(pairing, null, 2) + "\n");
|
|
1116
|
+
}
|
|
1117
|
+
var ENV_GUARD_KEYS = [
|
|
1118
|
+
"DB_DRIVER",
|
|
1119
|
+
"DATABASE_URL",
|
|
1120
|
+
"TURSO_DATABASE_URL",
|
|
1121
|
+
"TURSO_AUTH_TOKEN",
|
|
1122
|
+
"LIBSQL_AUTH_TOKEN",
|
|
1123
|
+
"WOD_WIKI_DB",
|
|
1124
|
+
"SQLITE_PATH",
|
|
1125
|
+
"POSTGRES_HOST",
|
|
1126
|
+
"POSTGRES_PORT",
|
|
1127
|
+
"POSTGRES_USER",
|
|
1128
|
+
"POSTGRES_PASSWORD",
|
|
1129
|
+
"POSTGRES_DATABASE",
|
|
1130
|
+
"POSTGRES_SSL"
|
|
1131
|
+
];
|
|
1132
|
+
function appendTursoEnv(envPath2, target, now) {
|
|
1133
|
+
assertSafePairingFields({ url: target.url, authToken: target.authToken, database: "wod-wiki" });
|
|
1134
|
+
const fs = fullFs();
|
|
1135
|
+
const existing = fs.existsSync(envPath2) ? fs.readFileSync(envPath2, "utf8") : "";
|
|
1136
|
+
const parsed = existing ? parseEnvContent(existing) : {};
|
|
1137
|
+
const present = ENV_GUARD_KEYS.filter((key) => typeof parsed[key] === "string" && parsed[key].trim() !== "");
|
|
1138
|
+
if (present.length > 0) {
|
|
1139
|
+
throw new Error(
|
|
1140
|
+
`[wod db] refusing to modify ${envPath2}: conflicting database selectors already set (${present.join(", ")}) \u2014 resolve them manually first; an appended Turso target would be ambiguous`
|
|
1141
|
+
);
|
|
1142
|
+
}
|
|
1143
|
+
const block = `
|
|
1144
|
+
# wod db pair \u2014 added ${now.toISOString()}
|
|
1145
|
+
DB_DRIVER=turso
|
|
1146
|
+
TURSO_DATABASE_URL=${target.url}
|
|
1147
|
+
TURSO_AUTH_TOKEN=${target.authToken}
|
|
1148
|
+
`;
|
|
1149
|
+
writeSecretFile(fs, envPath2, existing + block);
|
|
1150
|
+
}
|
|
1151
|
+
var DB_NAME_PATTERN = /^[a-z0-9][a-z0-9-]{0,63}$/;
|
|
1152
|
+
function assertDbName(name, what) {
|
|
1153
|
+
if (!DB_NAME_PATTERN.test(name)) {
|
|
1154
|
+
throw new Error(`[wod db] ${what} must match [a-z0-9][a-z0-9-]{0,63}, got "${name}"`);
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
function requireOutput(action, output) {
|
|
1158
|
+
if (!output || output === "-") {
|
|
1159
|
+
throw new Error(`[wod db ${action}] a pairing file path is required (-o <file>) \u2014 credentials are never printed`);
|
|
1160
|
+
}
|
|
1161
|
+
return output;
|
|
1162
|
+
}
|
|
1163
|
+
function requireSource(name, action) {
|
|
1164
|
+
if (!name) {
|
|
1165
|
+
throw new Error(`[wod db ${action}] missing source database name \u2014 usage: wod db ${action} <source> [options]`);
|
|
1166
|
+
}
|
|
1167
|
+
return name;
|
|
1168
|
+
}
|
|
1169
|
+
function requireGroup(action, group) {
|
|
1170
|
+
if (!group || group.trim() === "") {
|
|
1171
|
+
throw new Error(`[wod db ${action}] missing --group <name> \u2014 the Turso group must already exist`);
|
|
1172
|
+
}
|
|
1173
|
+
return group.trim();
|
|
1174
|
+
}
|
|
1175
|
+
function requireSourceGroup(action, info, args) {
|
|
1176
|
+
if (info.group) return info.group;
|
|
1177
|
+
if (args.group && args.group.trim() !== "") return args.group.trim();
|
|
1178
|
+
throw new Error(`[wod db ${action}] the Turso API did not report a group for "${info.name}" \u2014 pass --group <name> explicitly`);
|
|
1179
|
+
}
|
|
1180
|
+
function requireWritersRetired(action, source, ack) {
|
|
1181
|
+
if (!ack) {
|
|
1182
|
+
throw new Error(
|
|
1183
|
+
`[wod db ${action}] refusing to read a live source: retire its writers first (stop sync on every device), then pass --writers-retired to acknowledge "${source}" is frozen for the duration of the copy`
|
|
1184
|
+
);
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
async function openLibsql(url, authToken, deps) {
|
|
1188
|
+
const [{ createClient }, sql] = await Promise.all([
|
|
1189
|
+
import(
|
|
1190
|
+
/* @vite-ignore */
|
|
1191
|
+
'@libsql/client'
|
|
1192
|
+
),
|
|
1193
|
+
loadSqlLifecycle()
|
|
1194
|
+
]);
|
|
1195
|
+
const connectionUrl = deps.databaseUrl?.(new URL(url).hostname) ?? url;
|
|
1196
|
+
return sql.libsqlDatabase(createClient({ url: connectionUrl, authToken }));
|
|
1197
|
+
}
|
|
1198
|
+
async function closeQuietly(db) {
|
|
1199
|
+
await db.close().catch(() => void 0);
|
|
1200
|
+
}
|
|
1201
|
+
function targetUrl(hostname) {
|
|
1202
|
+
return `libsql://${hostname}`;
|
|
1203
|
+
}
|
|
1204
|
+
function snapshotValueEqual(a, b) {
|
|
1205
|
+
if (a === b) return true;
|
|
1206
|
+
if (typeof a === "number" && typeof b === "number") return a === b || Number.isNaN(a) && Number.isNaN(b);
|
|
1207
|
+
if (a === null || b === null || typeof a !== "object" || typeof b !== "object") return false;
|
|
1208
|
+
const aBytes = a instanceof ArrayBuffer ? new Uint8Array(a) : ArrayBuffer.isView(a) ? new Uint8Array(a.buffer, a.byteOffset, a.byteLength) : void 0;
|
|
1209
|
+
const bBytes = b instanceof ArrayBuffer ? new Uint8Array(b) : ArrayBuffer.isView(b) ? new Uint8Array(b.buffer, b.byteOffset, b.byteLength) : void 0;
|
|
1210
|
+
if (aBytes !== void 0 || bBytes !== void 0) {
|
|
1211
|
+
return aBytes !== void 0 && bBytes !== void 0 && aBytes.length === bBytes.length && aBytes.every((byte, i) => byte === bBytes[i]);
|
|
1212
|
+
}
|
|
1213
|
+
const aKeys = Object.keys(a);
|
|
1214
|
+
const bKeys = Object.keys(b);
|
|
1215
|
+
if (aKeys.length !== bKeys.length) return false;
|
|
1216
|
+
const aRecord = a;
|
|
1217
|
+
const bRecord = b;
|
|
1218
|
+
return aKeys.every((key) => key in bRecord && snapshotValueEqual(aRecord[key], bRecord[key]));
|
|
1219
|
+
}
|
|
1220
|
+
async function assertReplayMatches(sql, target, entries) {
|
|
1221
|
+
const ops = [];
|
|
1222
|
+
for (; ; ) {
|
|
1223
|
+
const window = await sql.fetchSyncOps(target, ops.length === 0 ? 0 : ops[ops.length - 1].rev, 1e4);
|
|
1224
|
+
if (window.length === 0) break;
|
|
1225
|
+
ops.push(...window);
|
|
1226
|
+
}
|
|
1227
|
+
const expected = entries.flatMap((entry, entryIndex) => entry.ops.map((mutation, idx) => ({ opId: `${entry.id}:${idx}`, rev: entryIndex + 1, idx, at: entry.at, mutation })));
|
|
1228
|
+
if (ops.length !== expected.length) {
|
|
1229
|
+
throw new Error(`[wod db migrate] replay verification failed: target has ${ops.length} ops, snapshot has ${expected.length}`);
|
|
1230
|
+
}
|
|
1231
|
+
for (let i = 0; i < expected.length; i++) {
|
|
1232
|
+
const got = ops[i];
|
|
1233
|
+
const want = expected[i];
|
|
1234
|
+
if (got.opId !== want.opId || got.rev !== want.rev || got.idx !== want.idx || got.at !== want.at || !snapshotValueEqual(got.mutation, want.mutation)) {
|
|
1235
|
+
throw new Error(`[wod db migrate] replay verification failed at op ${i} (${want.opId}) \u2014 target diverges from the validated snapshot`);
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
var OPS_PER_GROUP = 200;
|
|
1240
|
+
function classifySource(probe, source) {
|
|
1241
|
+
if (probe === "empty") throw new Error(`[wod db migrate] ${source} is empty \u2014 nothing to migrate`);
|
|
1242
|
+
if (probe.kind === "unknown") throw new Error(`[wod db migrate] ${source} holds foreign data \u2014 refusing to touch it`);
|
|
1243
|
+
if (probe.kind === "current") {
|
|
1244
|
+
if (probe.schemaVersion === 0) throw new Error(`[wod db migrate] ${source} is an incomplete migration target (constructing marker) \u2014 it was never validated`);
|
|
1245
|
+
if (probe.schemaVersion > DB_VERSION) throw new Error(`[wod db migrate] ${source} is schema v${probe.schemaVersion}, newer than this CLI (v${DB_VERSION}) \u2014 refusing`);
|
|
1246
|
+
if (probe.schemaVersion === DB_VERSION) throw new Error(`[wod db migrate] ${source} is already at schema v${DB_VERSION} \u2014 nothing to migrate`);
|
|
1247
|
+
return { fromVersion: probe.schemaVersion, legacy: false };
|
|
1248
|
+
}
|
|
1249
|
+
if (probe.schemaVersion > DB_VERSION) {
|
|
1250
|
+
throw new Error(`[wod db migrate] ${source} legacy schema v${probe.schemaVersion} is newer than this CLI (v${DB_VERSION}) \u2014 refusing`);
|
|
1251
|
+
}
|
|
1252
|
+
return { fromVersion: probe.schemaVersion, legacy: true };
|
|
1253
|
+
}
|
|
1254
|
+
async function convertSource(source, fromVersion, legacy) {
|
|
1255
|
+
const fake = await import(
|
|
1256
|
+
/* @vite-ignore */
|
|
1257
|
+
'fake-indexeddb'
|
|
1258
|
+
);
|
|
1259
|
+
const globals = globalThis;
|
|
1260
|
+
for (const [key, value] of Object.entries(fake)) {
|
|
1261
|
+
if (key !== "default") globals[key] = value;
|
|
1262
|
+
}
|
|
1263
|
+
globals.indexedDB = new fake.IDBFactory();
|
|
1264
|
+
const idb = await import(
|
|
1265
|
+
/* @vite-ignore */
|
|
1266
|
+
'@bitcobblers/wod-wiki-storage/idb'
|
|
1267
|
+
);
|
|
1268
|
+
return idb.convertSyncSource(source, fromVersion, legacy);
|
|
1269
|
+
}
|
|
1270
|
+
function snapshotEntries(mutations, snapshotId, at) {
|
|
1271
|
+
const entries = [];
|
|
1272
|
+
for (let start = 0; start < mutations.length; start += OPS_PER_GROUP) {
|
|
1273
|
+
entries.push({ id: `${snapshotId}-${entries.length}`, at, ops: mutations.slice(start, start + OPS_PER_GROUP) });
|
|
1274
|
+
}
|
|
1275
|
+
return entries;
|
|
1276
|
+
}
|
|
1277
|
+
async function runCreate(mgmt, args, deps, io, now) {
|
|
1278
|
+
const output = requireOutput("create", args.output);
|
|
1279
|
+
const group = requireGroup("create", args.group);
|
|
1280
|
+
const dbName = args.dbName ?? `wod-wiki-v${DB_VERSION}`;
|
|
1281
|
+
assertDbName(dbName, "database name");
|
|
1282
|
+
if (await mgmt.getDatabase(dbName)) {
|
|
1283
|
+
throw new Error(`[wod db create] database "${dbName}" already exists \u2014 pair it with \`wod db pair ${dbName}\` or choose --name`);
|
|
1284
|
+
}
|
|
1285
|
+
const created = await mgmt.createDatabase({ name: dbName, group });
|
|
1286
|
+
const url = targetUrl(created.hostname);
|
|
1287
|
+
const token = await mgmt.createToken(dbName, "full-access", "never");
|
|
1288
|
+
const db = await openLibsql(url, token, deps);
|
|
1289
|
+
let probe;
|
|
1290
|
+
try {
|
|
1291
|
+
probe = await (await loadSqlLifecycle()).probeSyncTarget(db);
|
|
1292
|
+
} finally {
|
|
1293
|
+
await closeQuietly(db);
|
|
1294
|
+
}
|
|
1295
|
+
if (probe !== "empty") {
|
|
1296
|
+
throw new Error(`[wod db create] freshly provisioned "${dbName}" probed as ${JSON.stringify(probe)}, expected empty \u2014 refusing to pair`);
|
|
1297
|
+
}
|
|
1298
|
+
const pairing = {
|
|
1299
|
+
wodWikiPairing: 1,
|
|
1300
|
+
kind: "turso",
|
|
1301
|
+
url,
|
|
1302
|
+
authToken: token,
|
|
1303
|
+
database: dbName,
|
|
1304
|
+
schemaVersion: DB_VERSION,
|
|
1305
|
+
createdAt: now.toISOString()
|
|
1306
|
+
};
|
|
1307
|
+
try {
|
|
1308
|
+
writePairingFile(output, pairing, { force: args.force });
|
|
1309
|
+
if (args.writeEnv) appendTursoEnv(envPath(args), { url, authToken: token }, now);
|
|
1310
|
+
} catch (error) {
|
|
1311
|
+
io.stderr(
|
|
1312
|
+
`[wod db create] pairing/config write failed (${error instanceof Error ? error.message : String(error)}) \u2014 the database "${dbName}" was created and was NOT deleted; recover with \`wod db pair ${dbName} --allow-empty -o <file>\`. No settings were changed.`
|
|
1313
|
+
);
|
|
1314
|
+
throw error;
|
|
1315
|
+
}
|
|
1316
|
+
io.stdout(`[wod db create] provisioned ${dbName} (${created.hostname}, group ${group}) \u2014 empty target ready for the browser's first bootstrap`);
|
|
1317
|
+
io.stdout(`[wod db create] pairing file: ${output}${args.writeEnv ? `; wrote ${envPath(args)}` : ""}`);
|
|
1318
|
+
}
|
|
1319
|
+
function envPath(args) {
|
|
1320
|
+
return args.envFile ?? ".env";
|
|
1321
|
+
}
|
|
1322
|
+
async function copyNative(kind, mgmt, args, deps, io, now) {
|
|
1323
|
+
const source = requireSource(args.name, kind);
|
|
1324
|
+
requireWritersRetired(kind, source, args.writersRetired);
|
|
1325
|
+
requireOutput(kind, args.output);
|
|
1326
|
+
const info = await mgmt.getDatabase(source);
|
|
1327
|
+
if (!info) throw new Error(`[wod db ${kind}] database not found: ${source}`);
|
|
1328
|
+
const copyName = args.dbName ?? `${sanitizeDbName(source)}-${kind}-${stamp(now)}`;
|
|
1329
|
+
assertDbName(copyName, "copy name");
|
|
1330
|
+
if (copyName === source) {
|
|
1331
|
+
throw new Error(`[wod db ${kind}] the copy must be a NEW database \u2014 "${copyName}" is the source itself`);
|
|
1332
|
+
}
|
|
1333
|
+
if (await mgmt.getDatabase(copyName)) {
|
|
1334
|
+
throw new Error(`[wod db ${kind}] target "${copyName}" already exists \u2014 choose --name`);
|
|
1335
|
+
}
|
|
1336
|
+
const sql = await loadSqlLifecycle();
|
|
1337
|
+
const group = requireSourceGroup(kind, info, args);
|
|
1338
|
+
const sourceToken = await mgmt.createToken(source, "read-only", "1h");
|
|
1339
|
+
const sourceDb = await openLibsql(targetUrl(info.hostname), sourceToken, deps);
|
|
1340
|
+
try {
|
|
1341
|
+
const before = await sql.databaseFingerprint(sourceDb);
|
|
1342
|
+
const copy = await mgmt.createDatabase({
|
|
1343
|
+
name: copyName,
|
|
1344
|
+
group,
|
|
1345
|
+
seed: { type: "database", name: source }
|
|
1346
|
+
});
|
|
1347
|
+
if (deps.midCopyHook) await deps.midCopyHook(copyName);
|
|
1348
|
+
const copyToken = await mgmt.createToken(copyName, "read-only", "1h");
|
|
1349
|
+
const copyDb = await openLibsql(targetUrl(copy.hostname), copyToken, deps);
|
|
1350
|
+
let after;
|
|
1351
|
+
try {
|
|
1352
|
+
after = await sql.assertDatabaseCopyMatches(sourceDb, copyDb, before);
|
|
1353
|
+
} finally {
|
|
1354
|
+
await closeQuietly(copyDb);
|
|
1355
|
+
}
|
|
1356
|
+
return finishCopy(kind, mgmt, args, io, now, copy, after);
|
|
1357
|
+
} finally {
|
|
1358
|
+
await closeQuietly(sourceDb);
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
async function finishCopy(kind, mgmt, args, io, now, copy, after) {
|
|
1362
|
+
io.stdout(
|
|
1363
|
+
`[wod db ${kind}] native copy "${copy.name}" verified: ${after.tables} tables, ${after.rows} rows, digest ${after.digest.slice(0, 8)} \u2014 source retained unchanged`
|
|
1364
|
+
);
|
|
1365
|
+
if (kind === "backup" || after.sync === void 0 || after.sync.schema !== DB_VERSION) {
|
|
1366
|
+
const schemaLabel = after.sync === void 0 ? "(legacy, no sync state)" : `v${after.sync.schema}`;
|
|
1367
|
+
io.stdout(
|
|
1368
|
+
`[wod db ${kind}] copy is schema ${schemaLabel} \u2014 retained as a read-only archive, NOT pairable for sync. Use \`wod db restore ${copy.name}\` for a writable current-schema copy or \`wod db migrate ${copy.name}\` to convert an older schema.`
|
|
1369
|
+
);
|
|
1370
|
+
if (args.output && args.output !== "-") {
|
|
1371
|
+
const roToken = await mgmt.createToken(copy.name, "read-only", "never");
|
|
1372
|
+
writePairingFile(args.output, {
|
|
1373
|
+
wodWikiPairing: 1,
|
|
1374
|
+
kind: "turso",
|
|
1375
|
+
url: targetUrl(copy.hostname),
|
|
1376
|
+
authToken: roToken,
|
|
1377
|
+
database: copy.name,
|
|
1378
|
+
schemaVersion: after.sync === void 0 ? 0 : after.sync.schema,
|
|
1379
|
+
createdAt: now.toISOString()
|
|
1380
|
+
}, { force: args.force });
|
|
1381
|
+
io.stdout(`[wod db ${kind}] read-only archive pairing file: ${args.output}`);
|
|
1382
|
+
}
|
|
1383
|
+
return { info: copy, fingerprint: after };
|
|
1384
|
+
}
|
|
1385
|
+
try {
|
|
1386
|
+
await pairTarget(mgmt, args, io, now, copy.name, copy.hostname, after.sync.schema);
|
|
1387
|
+
} catch (error) {
|
|
1388
|
+
io.stderr(
|
|
1389
|
+
`[wod db ${kind}] pairing/config write failed (${error instanceof Error ? error.message : String(error)}) \u2014 the verified copy "${copy.name}" was NOT deleted; recover with \`wod db pair ${copy.name} -o <file>\`. No settings were changed.`
|
|
1390
|
+
);
|
|
1391
|
+
throw error;
|
|
1392
|
+
}
|
|
1393
|
+
return { info: copy, fingerprint: after };
|
|
1394
|
+
}
|
|
1395
|
+
async function runMigrate(mgmt, args, deps, io, now) {
|
|
1396
|
+
const source = requireSource(args.name, "migrate");
|
|
1397
|
+
requireWritersRetired("migrate", source, args.writersRetired);
|
|
1398
|
+
const output = requireOutput("migrate", args.output);
|
|
1399
|
+
const info = await mgmt.getDatabase(source);
|
|
1400
|
+
if (!info) throw new Error(`[wod db migrate] database not found: ${source}`);
|
|
1401
|
+
const sql = await loadSqlLifecycle();
|
|
1402
|
+
const sourceToken = await mgmt.createToken(source, "read-only", "1h");
|
|
1403
|
+
const sourceDb = await openLibsql(targetUrl(info.hostname), sourceToken, deps);
|
|
1404
|
+
let before;
|
|
1405
|
+
let entries;
|
|
1406
|
+
try {
|
|
1407
|
+
before = await sql.databaseFingerprint(sourceDb);
|
|
1408
|
+
const classified = classifySource(await sql.probeSyncTarget(sourceDb), source);
|
|
1409
|
+
const mutations = await convertSource(sourceDb, classified.fromVersion, classified.legacy);
|
|
1410
|
+
if (mutations.length === 0) {
|
|
1411
|
+
throw new Error("[wod db migrate] conversion produced no rows \u2014 refusing to create an empty target");
|
|
1412
|
+
}
|
|
1413
|
+
entries = snapshotEntries(mutations, `m${classified.fromVersion}-${before.digest.slice(0, 8)}`, now.getTime());
|
|
1414
|
+
sql.assertPolicySnapshot(entries.flatMap((entry) => entry.ops));
|
|
1415
|
+
} finally {
|
|
1416
|
+
await closeQuietly(sourceDb);
|
|
1417
|
+
}
|
|
1418
|
+
const targetName = args.dbName ?? `${sanitizeDbName(source)}-v${DB_VERSION}`;
|
|
1419
|
+
assertDbName(targetName, "target name");
|
|
1420
|
+
if (targetName === source) {
|
|
1421
|
+
throw new Error("[wod db migrate] the target must be a NEW database \u2014 retention keeps the source at its own name");
|
|
1422
|
+
}
|
|
1423
|
+
if (await mgmt.getDatabase(targetName)) {
|
|
1424
|
+
throw new Error(`[wod db migrate] target "${targetName}" already exists \u2014 choose --name (a fresh, exclusively reserved target is required)`);
|
|
1425
|
+
}
|
|
1426
|
+
const created = await mgmt.createDatabase({ name: targetName, group: requireSourceGroup("migrate", info, args) });
|
|
1427
|
+
let token;
|
|
1428
|
+
let committed;
|
|
1429
|
+
try {
|
|
1430
|
+
token = await mgmt.createToken(targetName, "full-access", "never");
|
|
1431
|
+
const target = await openLibsql(targetUrl(created.hostname), token, deps);
|
|
1432
|
+
try {
|
|
1433
|
+
const probe = await sql.probeSyncTarget(target);
|
|
1434
|
+
if (probe !== "empty") throw new Error(`[wod db migrate] target "${targetName}" is not empty (${JSON.stringify(probe)}) \u2014 refusing`);
|
|
1435
|
+
await sql.initSyncTarget(target, 0);
|
|
1436
|
+
committed = await sql.applySyncBatch(target, entries, { expectSchema: 0 });
|
|
1437
|
+
await assertReplayMatches(sql, target, entries);
|
|
1438
|
+
const sourceDbRecheck = await openLibsql(targetUrl(info.hostname), sourceToken, deps);
|
|
1439
|
+
try {
|
|
1440
|
+
await sql.assertDatabaseCopyMatches(sourceDbRecheck, sourceDbRecheck, before);
|
|
1441
|
+
} finally {
|
|
1442
|
+
await closeQuietly(sourceDbRecheck);
|
|
1443
|
+
}
|
|
1444
|
+
await sql.finalizeMigrationTarget(target, DB_VERSION, committed);
|
|
1445
|
+
} finally {
|
|
1446
|
+
await closeQuietly(target);
|
|
1447
|
+
}
|
|
1448
|
+
} catch (error) {
|
|
1449
|
+
io.stderr(
|
|
1450
|
+
`[wod db migrate] target "${targetName}" was left INCOMPLETE and is not pairable (constructing marker). Inspect or remove it: \`wod db delete ${targetName} --yes ${targetName}\`. The source "${source}" was not modified.`
|
|
1451
|
+
);
|
|
1452
|
+
throw error;
|
|
1453
|
+
}
|
|
1454
|
+
try {
|
|
1455
|
+
writePairingFile(output, {
|
|
1456
|
+
wodWikiPairing: 1,
|
|
1457
|
+
kind: "turso",
|
|
1458
|
+
url: targetUrl(created.hostname),
|
|
1459
|
+
authToken: token,
|
|
1460
|
+
database: targetName,
|
|
1461
|
+
schemaVersion: DB_VERSION,
|
|
1462
|
+
createdAt: now.toISOString()
|
|
1463
|
+
}, { force: args.force });
|
|
1464
|
+
if (args.writeEnv) appendTursoEnv(envPath(args), { url: targetUrl(created.hostname), authToken: token }, now);
|
|
1465
|
+
} catch (error) {
|
|
1466
|
+
io.stderr(
|
|
1467
|
+
`[wod db migrate] pairing/config write failed (${error instanceof Error ? error.message : String(error)}) \u2014 the target "${targetName}" is complete at v${DB_VERSION} and was NOT deleted; recover with \`wod db pair ${targetName} -o <file>\`. No settings were changed.`
|
|
1468
|
+
);
|
|
1469
|
+
throw error;
|
|
1470
|
+
}
|
|
1471
|
+
io.stdout(
|
|
1472
|
+
`[wod db migrate] ${source} (schema ${before.sync ? `v${before.sync.schema}` : "legacy"}) \u2192 ${targetName} at v${DB_VERSION}: ${entries.length} groups / ${committed} committed, replay + pins + source CAS verified \u2014 source retained`
|
|
1473
|
+
);
|
|
1474
|
+
io.stdout(`[wod db migrate] pairing file: ${output}${args.writeEnv ? `; wrote ${envPath(args)}` : ""}`);
|
|
1475
|
+
}
|
|
1476
|
+
async function runDelete(args, io, deps) {
|
|
1477
|
+
const name = requireSource(args.name, "delete");
|
|
1478
|
+
if (!args.yes) {
|
|
1479
|
+
throw new Error(`[wod db delete] deletion requires exact confirmation: wod db delete ${name} --yes ${name}`);
|
|
1480
|
+
}
|
|
1481
|
+
if (args.yes !== name) {
|
|
1482
|
+
throw new Error(`[wod db delete] confirmation mismatch: --yes "${args.yes}" does not exactly match "${name}" \u2014 nothing was deleted`);
|
|
1483
|
+
}
|
|
1484
|
+
await TursoManagement.fromEnv(args.env, { fetchFn: deps.fetchFn }).deleteDatabase(name);
|
|
1485
|
+
io.stdout(`[wod db delete] deleted ${name}`);
|
|
1486
|
+
}
|
|
1487
|
+
async function pairTarget(mgmt, args, io, now, dbName, hostname, schemaVersion) {
|
|
1488
|
+
const output = requireOutput("pair", args.output);
|
|
1489
|
+
const token = await mgmt.createToken(dbName, "full-access", "never");
|
|
1490
|
+
const url = targetUrl(hostname);
|
|
1491
|
+
writePairingFile(output, {
|
|
1492
|
+
wodWikiPairing: 1,
|
|
1493
|
+
kind: "turso",
|
|
1494
|
+
url,
|
|
1495
|
+
authToken: token,
|
|
1496
|
+
database: dbName,
|
|
1497
|
+
schemaVersion,
|
|
1498
|
+
createdAt: now.toISOString()
|
|
1499
|
+
}, { force: args.force });
|
|
1500
|
+
if (args.writeEnv) appendTursoEnv(envPath(args), { url, authToken: token }, now);
|
|
1501
|
+
io.stdout(`[wod db pair] ${dbName} (${hostname}) at schema v${schemaVersion} \u2014 pairing file: ${output}${args.writeEnv ? `; wrote ${envPath(args)}` : ""}`);
|
|
1502
|
+
}
|
|
1503
|
+
async function runPair(mgmt, args, deps, io, now) {
|
|
1504
|
+
const name = requireSource(args.name, "pair");
|
|
1505
|
+
const info = await mgmt.getDatabase(name);
|
|
1506
|
+
if (!info) throw new Error(`[wod db pair] database not found: ${name}`);
|
|
1507
|
+
const sql = await loadSqlLifecycle();
|
|
1508
|
+
const probeToken = await mgmt.createToken(name, "read-only", "1h");
|
|
1509
|
+
const db = await openLibsql(targetUrl(info.hostname), probeToken, deps);
|
|
1510
|
+
let probe;
|
|
1511
|
+
try {
|
|
1512
|
+
await db.query("SELECT 1");
|
|
1513
|
+
probe = await sql.probeSyncTarget(db);
|
|
1514
|
+
if (probe !== "empty" && probe.kind === "current") {
|
|
1515
|
+
const ops = [];
|
|
1516
|
+
let cursor = 0;
|
|
1517
|
+
for (; ; ) {
|
|
1518
|
+
const window = await sql.fetchSyncOps(db, cursor, 1e4);
|
|
1519
|
+
if (window.length === 0) break;
|
|
1520
|
+
ops.push(...window);
|
|
1521
|
+
cursor = window[window.length - 1].rev;
|
|
1522
|
+
}
|
|
1523
|
+
sql.assertLogPinClosure(ops);
|
|
1524
|
+
}
|
|
1525
|
+
} finally {
|
|
1526
|
+
await closeQuietly(db);
|
|
1527
|
+
}
|
|
1528
|
+
if (probe === "empty") {
|
|
1529
|
+
if (!args.allowEmpty) {
|
|
1530
|
+
throw new Error(`[wod db pair] ${name} is empty \u2014 it is only pairable for a deliberate first bootstrap (--allow-empty), after \`wod db create\`, or once a browser initialized it`);
|
|
1531
|
+
}
|
|
1532
|
+
await pairTarget(mgmt, args, io, now, name, info.hostname, DB_VERSION);
|
|
1533
|
+
return;
|
|
1534
|
+
}
|
|
1535
|
+
if (probe.kind === "unknown") {
|
|
1536
|
+
throw new Error(`[wod db pair] ${name} holds foreign data \u2014 refusing to pair`);
|
|
1537
|
+
}
|
|
1538
|
+
if (probe.kind === "legacy") {
|
|
1539
|
+
throw new Error(`[wod db pair] ${name} is a legacy direct-Turso database (v${probe.schemaVersion}) \u2014 run \`wod db migrate ${name}\` first`);
|
|
1540
|
+
}
|
|
1541
|
+
if (probe.schemaVersion === 0) {
|
|
1542
|
+
throw new Error(`[wod db pair] ${name} is an incomplete migration target (constructing marker) \u2014 it was never validated and stays unpairable`);
|
|
1543
|
+
}
|
|
1544
|
+
if (probe.schemaVersion !== DB_VERSION) {
|
|
1545
|
+
throw new Error(`[wod db pair] ${name} is schema v${probe.schemaVersion}, this CLI manages v${DB_VERSION} \u2014 run \`wod db migrate ${name}\` first`);
|
|
1546
|
+
}
|
|
1547
|
+
await pairTarget(mgmt, args, io, now, name, info.hostname, probe.schemaVersion);
|
|
1548
|
+
}
|
|
1549
|
+
function sanitizeDbName(raw) {
|
|
1550
|
+
const cleaned = raw.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+/, "").replace(/-+$/, "");
|
|
1551
|
+
return cleaned === "" ? "wod-wiki" : cleaned.slice(0, 64).replace(/-+$/, "");
|
|
1552
|
+
}
|
|
1553
|
+
function stamp(now) {
|
|
1554
|
+
return now.toISOString().replace(/[-:]/g, "").replace(/\.\d+Z$/, "Z").toLowerCase();
|
|
1555
|
+
}
|
|
1556
|
+
async function runDbCommand(args, io, deps = {}) {
|
|
1557
|
+
const now = (deps.now ?? (() => /* @__PURE__ */ new Date()))();
|
|
1558
|
+
const mgmt = () => TursoManagement.fromEnv(args.env, { fetchFn: deps.fetchFn });
|
|
1559
|
+
switch (args.action) {
|
|
1560
|
+
case "create":
|
|
1561
|
+
return runCreate(mgmt(), args, deps, io, now);
|
|
1562
|
+
case "backup":
|
|
1563
|
+
case "restore":
|
|
1564
|
+
await copyNative(args.action, mgmt(), args, deps, io, now);
|
|
1565
|
+
return;
|
|
1566
|
+
case "migrate":
|
|
1567
|
+
return runMigrate(mgmt(), args, deps, io, now);
|
|
1568
|
+
case "delete":
|
|
1569
|
+
return runDelete(args, io, deps);
|
|
1570
|
+
case "pair":
|
|
1571
|
+
return runPair(mgmt(), args, deps, io, now);
|
|
1572
|
+
default:
|
|
1573
|
+
throw new Error(
|
|
1574
|
+
`[wod db] unknown action "${args.action ?? ""}" \u2014 usage: wod db <create|backup|restore|migrate|delete|pair> [name] [options]`
|
|
1575
|
+
);
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
926
1578
|
var registeredWidgets = KNOWN_SECTION_WIDGETS;
|
|
927
1579
|
var saveStores = ["notes", "block_revisions", "block_index", "block_efforts", "block_equipment", "efforts", "equipment", "tags", "note_tags", "sessions"];
|
|
928
1580
|
function titleFromLabel(sourceLabel) {
|
|
@@ -1440,6 +2092,9 @@ ${fence}
|
|
|
1440
2092
|
var VALUE_FLAGS = [
|
|
1441
2093
|
["--db", "db"],
|
|
1442
2094
|
["--db-driver", "dbDriver"],
|
|
2095
|
+
["--group", "group"],
|
|
2096
|
+
["--name", "dbName"],
|
|
2097
|
+
["--yes", "yes"],
|
|
1443
2098
|
["--database-url", "databaseUrl"],
|
|
1444
2099
|
["--postgres-host", "postgresHost"],
|
|
1445
2100
|
["--postgres-port", "postgresPort"],
|
|
@@ -1546,6 +2201,18 @@ function parseCliArgs(args) {
|
|
|
1546
2201
|
} else if (arg === "--scaffold") {
|
|
1547
2202
|
result.scaffold = true;
|
|
1548
2203
|
i++;
|
|
2204
|
+
} else if (arg === "--writers-retired") {
|
|
2205
|
+
result.writersRetired = true;
|
|
2206
|
+
i++;
|
|
2207
|
+
} else if (arg === "--allow-empty") {
|
|
2208
|
+
result.allowEmpty = true;
|
|
2209
|
+
i++;
|
|
2210
|
+
} else if (arg === "--write-env") {
|
|
2211
|
+
result.writeEnv = true;
|
|
2212
|
+
i++;
|
|
2213
|
+
} else if (arg === "--force") {
|
|
2214
|
+
result.force = true;
|
|
2215
|
+
i++;
|
|
1549
2216
|
} else if (arg === "--template") {
|
|
1550
2217
|
result.template = args[++i];
|
|
1551
2218
|
i++;
|
|
@@ -1572,7 +2239,7 @@ function parseCliArgs(args) {
|
|
|
1572
2239
|
if (Number.isSafeInteger(v) && v >= 0) result.seedVersion = v;
|
|
1573
2240
|
i++;
|
|
1574
2241
|
} else if (!result.subcommand) {
|
|
1575
|
-
if (arg === "parse" || arg === "run" || arg === "query" || arg === "download" || arg === "export" || arg === "import" || arg === "publish" || arg === "build" || arg === "seed" || arg === "reset" || arg === "init" || arg === "server" || arg === "serve" || arg === "playground" || arg === "init-catalog" || arg === "help" || arg === "version") {
|
|
2242
|
+
if (arg === "parse" || arg === "run" || arg === "query" || arg === "download" || arg === "export" || arg === "import" || arg === "publish" || arg === "build" || arg === "seed" || arg === "reset" || arg === "init" || arg === "db" || arg === "server" || arg === "serve" || arg === "playground" || arg === "init-catalog" || arg === "help" || arg === "version") {
|
|
1576
2243
|
result.subcommand = arg;
|
|
1577
2244
|
} else {
|
|
1578
2245
|
result.target = arg;
|
|
@@ -1645,6 +2312,47 @@ USAGE:
|
|
|
1645
2312
|
wod seed [options]
|
|
1646
2313
|
wod reset [options]
|
|
1647
2314
|
wod init Initialize the configured database (idempotent)
|
|
2315
|
+
wod db <action> [name] [options]
|
|
2316
|
+
Turso database lifecycle (management credentials from
|
|
2317
|
+
the environment only: TURSO_API_TOKEN + TURSO_ORGANIZATION;
|
|
2318
|
+
never flags, never printed):
|
|
2319
|
+
wod db create [--name <db>] --group <g> -o <pairing-file>
|
|
2320
|
+
Provision a fresh empty target (default name
|
|
2321
|
+
wod-wiki-v<schema>) and write its pairing file.
|
|
2322
|
+
wod db backup <source> [--name <copy>] --writers-retired -o <file>
|
|
2323
|
+
wod db restore <source> [--name <copy>] --writers-retired -o <file>
|
|
2324
|
+
Native whole-database copy (Control API seed); verified
|
|
2325
|
+
row- and binary-identical before any token is minted.
|
|
2326
|
+
Backups receive read-only archive credentials. Restore
|
|
2327
|
+
copies are writable only at the current schema;
|
|
2328
|
+
older-schema copies stay read-only until migrated.
|
|
2329
|
+
wod db migrate <source> [--name <target>] --writers-retired -o <file>
|
|
2330
|
+
Logical conversion to a fresh current-schema target via
|
|
2331
|
+
the shared IndexedDB adapter. The target stays
|
|
2332
|
+
unpairable (constructing marker) until replay, pin and
|
|
2333
|
+
source-immutability checks pass; the source is retained.
|
|
2334
|
+
wod db delete <name> --yes <name>
|
|
2335
|
+
Delete a database \u2014 --yes must repeat the exact name.
|
|
2336
|
+
wod db pair <name> [--allow-empty] -o <file> [--write-env]
|
|
2337
|
+
Mint a database-scoped token and write the pairing file
|
|
2338
|
+
for the browser Data settings (0600, exclusive create;
|
|
2339
|
+
--force replaces atomically). Refuses foreign, legacy,
|
|
2340
|
+
newer and unvalidated targets. --write-env opt-in
|
|
2341
|
+
appends DB_DRIVER/TURSO_* to the .env atomically and
|
|
2342
|
+
refuses to replace existing database settings.
|
|
2343
|
+
|
|
2344
|
+
DB OPTIONS (for 'wod db'):
|
|
2345
|
+
--name <db> Explicit target/copy database name
|
|
2346
|
+
--group <g> Turso group for 'create' (must exist); fallback target
|
|
2347
|
+
group when the API does not report one
|
|
2348
|
+
--writers-retired Acknowledge all writers on the source were stopped
|
|
2349
|
+
before backup/restore/migrate read it
|
|
2350
|
+
--allow-empty 'pair' a deliberately empty bootstrap target
|
|
2351
|
+
--write-env Opt-in .env output (atomic, refuses replacement)
|
|
2352
|
+
--force Replace an existing pairing file (stays 0600)
|
|
2353
|
+
-o, --output <file> Pairing file path \u2014 REQUIRED for credential-issuing
|
|
2354
|
+
actions; credentials are never printed to stdout
|
|
2355
|
+
|
|
1648
2356
|
wod server [options]
|
|
1649
2357
|
Serve the app (HTTP API + packaged frontend; alias: serve)
|
|
1650
2358
|
wod playground [options]
|
|
@@ -1773,6 +2481,7 @@ ENVIRONMENT (flags > process env > .env):
|
|
|
1773
2481
|
POSTGRES_HOST, POSTGRES_PORT, POSTGRES_USER,
|
|
1774
2482
|
POSTGRES_PASSWORD, POSTGRES_DATABASE, POSTGRES_SSL
|
|
1775
2483
|
TURSO_DATABASE_URL, TURSO_AUTH_TOKEN, LIBSQL_AUTH_TOKEN
|
|
2484
|
+
TURSO_API_TOKEN, TURSO_ORGANIZATION wod db management credentials (env only)
|
|
1776
2485
|
HOST, PORT, PLAYGROUND_DIST server / playground defaults
|
|
1777
2486
|
|
|
1778
2487
|
EXIT CODES:
|
|
@@ -1849,7 +2558,7 @@ async function cliMain(argv = process.argv.slice(2), io = {}) {
|
|
|
1849
2558
|
writeErr("[wod init-catalog] Error: Missing catalog name (a single new directory name)");
|
|
1850
2559
|
return 1;
|
|
1851
2560
|
}
|
|
1852
|
-
const { scaffoldCatalog } = await import('./scaffoldCatalog-
|
|
2561
|
+
const { scaffoldCatalog } = await import('./scaffoldCatalog-APBMVJES.mjs');
|
|
1853
2562
|
try {
|
|
1854
2563
|
const result = scaffoldCatalog(parsed.target);
|
|
1855
2564
|
writeOut(`[wod init-catalog] Created ${result.dir} (${result.files.length} files)`);
|
|
@@ -2286,6 +2995,28 @@ async function cliMain(argv = process.argv.slice(2), io = {}) {
|
|
|
2286
2995
|
}
|
|
2287
2996
|
return 0;
|
|
2288
2997
|
}
|
|
2998
|
+
if (parsed.subcommand === "db") {
|
|
2999
|
+
try {
|
|
3000
|
+
await runDbCommand({
|
|
3001
|
+
action: parsed.target,
|
|
3002
|
+
name: parsed.target2,
|
|
3003
|
+
dbName: parsed.dbName,
|
|
3004
|
+
group: parsed.group,
|
|
3005
|
+
yes: parsed.yes,
|
|
3006
|
+
output: parsed.output,
|
|
3007
|
+
writeEnv: parsed.writeEnv === true,
|
|
3008
|
+
writersRetired: parsed.writersRetired === true,
|
|
3009
|
+
allowEmpty: parsed.allowEmpty === true,
|
|
3010
|
+
force: parsed.force === true,
|
|
3011
|
+
envFile: parsed.envFile,
|
|
3012
|
+
env: cliEnv()
|
|
3013
|
+
}, { stdout: writeOut, stderr: writeErr }, io.dbDeps);
|
|
3014
|
+
return 0;
|
|
3015
|
+
} catch (error) {
|
|
3016
|
+
writeErr(error instanceof Error ? error.message : String(error));
|
|
3017
|
+
return 1;
|
|
3018
|
+
}
|
|
3019
|
+
}
|
|
2289
3020
|
if (parsed.subcommand === "server" || parsed.subcommand === "serve" || parsed.subcommand === "playground") {
|
|
2290
3021
|
const mode = parsed.subcommand === "playground" ? "playground" : "server";
|
|
2291
3022
|
const { runCliServer } = await import('./server-2DVBA35J.mjs');
|
|
@@ -2314,5 +3045,5 @@ async function cliMain(argv = process.argv.slice(2), io = {}) {
|
|
|
2314
3045
|
}
|
|
2315
3046
|
|
|
2316
3047
|
export { PackLoadError, ParseSyntaxError, WqlSyntaxError, cliMain, formatExecutionOutput, formatParseOutput, formatQueryOutput, loadCliEnvironment, loadLanguagePack, loadQueryData, parseCliArgs, readStdin, resolveDatabaseConfig, runExecution, runParse, runQueryCli };
|
|
2317
|
-
//# sourceMappingURL=chunk-
|
|
2318
|
-
//# sourceMappingURL=chunk-
|
|
3048
|
+
//# sourceMappingURL=chunk-464KHLHY.mjs.map
|
|
3049
|
+
//# sourceMappingURL=chunk-464KHLHY.mjs.map
|