planche 0.7.13 → 0.7.15

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.
Files changed (71) hide show
  1. package/app/assets/{AboutDialog-CxhMAbHE.js → AboutDialog-DJ1xTjQa.js} +1 -1
  2. package/app/assets/ConnectDialog-CsyQZTfv.js +1 -0
  3. package/app/assets/CopyDatabaseDialog-DVaHAD1q.js +1 -0
  4. package/app/assets/{CopyTableDialog-y2yVCEoe.js → CopyTableDialog-BGaxEeFf.js} +1 -1
  5. package/app/assets/CreateDatabaseDialog-DiwtxqWI.js +1 -0
  6. package/app/assets/CreateSchemaDialog-CPhHOdED.js +1 -0
  7. package/app/assets/{DataDiffDialog-BQvHEwN1.js → DataDiffDialog-BQzfzN_-.js} +1 -1
  8. package/app/assets/DownloadToCSVDialog-CMQ_k6oK.js +3 -0
  9. package/app/assets/EditSchemaDialog-e9w9yyIn.js +2 -0
  10. package/app/assets/FindReplaceDialog-BbbQS-3c.js +1 -0
  11. package/app/assets/{FlushDialog-DgeG-OGF.js → FlushDialog-D_tK375i.js} +1 -1
  12. package/app/assets/{LinkColumnDialog-DBbb55b5.js → LinkColumnDialog-Df-_8K5T.js} +1 -1
  13. package/app/assets/{LocalHistoryDialog-BZcsP3oj.js → LocalHistoryDialog-B3Yww-nk.js} +1 -1
  14. package/app/assets/{ProcessListDialog-DpH-Mybn.js → ProcessListDialog-6YJn364S.js} +1 -1
  15. package/app/assets/{QuickCommandPalette-CjZVdpv1.js → QuickCommandPalette-Cvvri_sm.js} +1 -1
  16. package/app/assets/{RowFormDialog-Cb8MAzWF.js → RowFormDialog-B5yd3Kv7.js} +1 -1
  17. package/app/assets/{SafetySettingsDialog-fO9X1PFB.js → SafetySettingsDialog-ce5eqfIE.js} +1 -1
  18. package/app/assets/{SchemaDiffDialog-DdRHijgZ.js → SchemaDiffDialog-CAU4Tko1.js} +1 -1
  19. package/app/assets/{SchemaToHTMLDialog-HbVvnUOc.js → SchemaToHTMLDialog-Bp8NQ4S0.js} +1 -1
  20. package/app/assets/{StatusDialog-B2pVC65w.js → StatusDialog-CT8E2pK0.js} +1 -1
  21. package/app/assets/{TermsDialog-B8Ehf7Ck.js → TermsDialog-BzlyiBiw.js} +1 -1
  22. package/app/assets/{UserAddDialog-CzmcUU7I.js → UserAddDialog-CyK4YAvW.js} +1 -1
  23. package/app/assets/UserManagerDialog-oRNIMr07.js +1 -0
  24. package/app/assets/{VariablesDialog-Dg5CDmWn.js → VariablesDialog-bHfrRqC3.js} +1 -1
  25. package/app/assets/app-storage-C_1RLCyI.js +1 -0
  26. package/app/assets/{connection-store-CAPTzFtT.js → connection-store-DjSajhjh.js} +1 -1
  27. package/app/assets/database-BUN6Jvbi.js +1 -0
  28. package/app/assets/{dbms-Bsjvyvsp.js → dbms-B9Zk7nEm.js} +8 -8
  29. package/app/assets/de-sebDJePc.js +2 -0
  30. package/app/assets/editor-registry-C_yarsyY.js +1 -0
  31. package/app/assets/{engine-DWYEiSZM.js → engine-C2ZJalx5.js} +1 -1
  32. package/app/assets/fr-CIPqitfo.js +2 -0
  33. package/app/assets/hmr-singleton-Bit0Kfog.js +1 -0
  34. package/app/assets/{i18n-NLG6supM.js → i18n-BBdlx4Gw.js} +2 -2
  35. package/app/assets/ja-Bf1tvh4M.js +2 -0
  36. package/app/assets/ko-D8httUGO.js +2 -0
  37. package/app/assets/main-BjGObGVl.js +940 -0
  38. package/app/assets/main-DaPLBcIe.css +2 -0
  39. package/app/assets/pro-usage-dBNmC2gt.js +1 -0
  40. package/app/assets/query-execution-DqpMGs8z.js +7 -0
  41. package/app/assets/{query-store-CRp9AQS5.js → query-store-Bz1pmp4E.js} +1 -1
  42. package/app/assets/{schema-nodes-D3-cz2Xk.js → schema-nodes-XgnUaB2x.js} +1 -1
  43. package/app/assets/{select-DAm5iaPc.js → select-BFa0--sE.js} +1 -1
  44. package/app/assets/{settings-store-CYHWBgNo.js → settings-store-CSRx4hwh.js} +1 -1
  45. package/app/assets/table-copy-W19HZGKM.js +1 -0
  46. package/app/assets/use-engine-C_C2qpfD.js +1 -0
  47. package/app/assets/zh-DAWEX6ju.js +2 -0
  48. package/app/index.html +21 -17
  49. package/app/landing.css +13 -6
  50. package/dist/index.js +611 -23
  51. package/package.json +9 -4
  52. package/app/assets/ConnectDialog-Dk5tee_2.js +0 -1
  53. package/app/assets/CopyDatabaseDialog-BwviLOYR.js +0 -1
  54. package/app/assets/CreateDatabaseDialog-v63m43WC.js +0 -1
  55. package/app/assets/DownloadToCSVDialog-DGQnI5eb.js +0 -3
  56. package/app/assets/EditSchemaDialog-DhC0D6ws.js +0 -1
  57. package/app/assets/FindReplaceDialog-CluDq2uv.js +0 -1
  58. package/app/assets/UserManagerDialog-YBOODqbg.js +0 -1
  59. package/app/assets/app-storage-B6v72zLH.js +0 -1
  60. package/app/assets/database-ezjCHdX9.js +0 -1
  61. package/app/assets/de-DP-iF2aJ.js +0 -2
  62. package/app/assets/fr-DFtpsn6X.js +0 -2
  63. package/app/assets/ja--An7okvc.js +0 -2
  64. package/app/assets/ko-C-OjtONK.js +0 -2
  65. package/app/assets/main-Cs_AF7Zo.js +0 -940
  66. package/app/assets/main-pmntwMQg.css +0 -2
  67. package/app/assets/pro-usage-VlZJ74Cl.js +0 -1
  68. package/app/assets/query-execution-BZE0rVpK.js +0 -7
  69. package/app/assets/table-copy-93YWKupe.js +0 -1
  70. package/app/assets/use-engine-DSWrApw6.js +0 -1
  71. package/app/assets/zh-C4dUMxzb.js +0 -2
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { randomUUID as randomUUID2 } from "crypto";
4
+ import { randomUUID as randomUUID3 } from "crypto";
5
5
  import { createRequire as createRequire2 } from "module";
6
6
  import { spawn } from "child_process";
7
7
  import { parseArgs } from "util";
@@ -61,10 +61,12 @@ var USE_STATEMENT = /^\s*USE\s/i;
61
61
  var MySqlDriver = class {
62
62
  constructor(connection, config) {
63
63
  this.connection = connection;
64
+ this.config = config;
64
65
  this.label = `${config.user}@${config.host}:${config.port}`;
65
66
  this.currentDatabase = config.database ?? null;
66
67
  }
67
68
  connection;
69
+ config;
68
70
  type = "mysql";
69
71
  label;
70
72
  /**
@@ -110,6 +112,21 @@ var MySqlDriver = class {
110
112
  async setReadOnly() {
111
113
  await this.connection.query("SET SESSION TRANSACTION READ ONLY");
112
114
  }
115
+ /**
116
+ * A connection running a statement cannot take another one, so the KILL has
117
+ * to come down a second connection. `KILL QUERY` ends the statement and
118
+ * leaves the session — and its open transaction — alone.
119
+ */
120
+ async cancel() {
121
+ const threadId = this.connection.threadId;
122
+ if (!threadId) return;
123
+ const killer = await mysql.createConnection(toConnectionOptions(this.config));
124
+ try {
125
+ await killer.query(`KILL QUERY ${Number(threadId)}`);
126
+ } finally {
127
+ await killer.end().catch(() => void 0);
128
+ }
129
+ }
113
130
  async close() {
114
131
  await this.connection.end();
115
132
  }
@@ -219,9 +236,11 @@ var quoteIdent = (name) => `"${name.replaceAll('"', '""')}"`;
219
236
  var PostgresDriver = class {
220
237
  constructor(client, config) {
221
238
  this.client = client;
239
+ this.config = config;
222
240
  this.label = `${config.user}@${config.host}:${config.port}/${config.database}`;
223
241
  }
224
242
  client;
243
+ config;
225
244
  type = "postgres";
226
245
  label;
227
246
  /**
@@ -306,6 +325,21 @@ var PostgresDriver = class {
306
325
  async setReadOnly() {
307
326
  await this.client.query("SET default_transaction_read_only = on");
308
327
  }
328
+ /**
329
+ * `pg_cancel_backend` from a second connection: the busy one cannot carry the
330
+ * request. It cancels the running statement only, leaving the session open.
331
+ */
332
+ async cancel() {
333
+ const pid = this.client.processID;
334
+ if (!pid) return;
335
+ const canceller = new pg.Client(toClientConfig(this.config));
336
+ try {
337
+ await canceller.connect();
338
+ await canceller.query("SELECT pg_cancel_backend($1)", [pid]);
339
+ } finally {
340
+ await canceller.end().catch(() => void 0);
341
+ }
342
+ }
309
343
  async close() {
310
344
  await this.client.end();
311
345
  }
@@ -641,10 +675,22 @@ var MssqlDriver = class {
641
675
  this.sources.set(text, result);
642
676
  return result;
643
677
  }
678
+ /** The request in flight, so a cancel can interrupt it. */
679
+ running = null;
680
+ /** `tedious` sends an attention packet; the statement ends and the pool stays usable. */
681
+ async cancel() {
682
+ this.running?.cancel();
683
+ }
644
684
  async query(text) {
645
685
  const req = this.pool.request();
646
686
  req.arrayRowMode = true;
647
- const res = await req.query(text);
687
+ this.running = req;
688
+ let res;
689
+ try {
690
+ res = await req.query(text);
691
+ } finally {
692
+ if (this.running === req) this.running = null;
693
+ }
648
694
  const sets = res.recordsets;
649
695
  const last = sets.length > 0 ? sets[sets.length - 1] : void 0;
650
696
  const columns = last?.columns;
@@ -817,6 +863,14 @@ var OracleDriver = class {
817
863
  }
818
864
  return { fields: [], records: [], affectedRows: res.rowsAffected ?? 0, insertId: 0, isResultQuery: false };
819
865
  }
866
+ /**
867
+ * `break()` interrupts the call in progress; the session then needs
868
+ * `reset()` before it can be used again.
869
+ */
870
+ async cancel() {
871
+ await this.conn.break();
872
+ await this.conn.reset?.().catch(() => void 0);
873
+ }
820
874
  async close() {
821
875
  await this.conn.close();
822
876
  }
@@ -1028,6 +1082,468 @@ var d1DriverFactory = {
1028
1082
  }
1029
1083
  };
1030
1084
 
1085
+ // src/drivers/do.ts
1086
+ var LOCAL_EXPLORER = "/cdn-cgi/local/explorer/api";
1087
+ var CLOUDFLARE_API = "https://api.cloudflare.com/client/v4";
1088
+ var MAX_OBJECTS = 5e3;
1089
+ var DoError = class extends Error {
1090
+ constructor(message, status) {
1091
+ super(message);
1092
+ this.status = status;
1093
+ }
1094
+ status;
1095
+ };
1096
+ var DATABASE_LIST2 = /^\s*PRAGMA\s+database_list\s*;?\s*$/i;
1097
+ var RESULT_KIND2 = /^\s*(SELECT|WITH|PRAGMA|EXPLAIN|VALUES)\b/i;
1098
+ var TRANSACTION = /^\s*(BEGIN|COMMIT|END|ROLLBACK|SAVEPOINT|RELEASE)\b/i;
1099
+ var DDL = /^\s*(CREATE|ALTER|DROP)\b/i;
1100
+ var HEX_ID = /^[0-9a-f]{64}$/i;
1101
+ function singleSourceTable3(sql2) {
1102
+ const m = /\bFROM\s+("[^"]+"|`[^`]+`|\[[^\]]+\]|[\w$]+)([\s\S]*)$/i.exec(sql2);
1103
+ if (!m) return null;
1104
+ const rest = m[2].replace(
1105
+ /\b(WHERE|ORDER\s+BY|GROUP\s+BY|HAVING|LIMIT|OFFSET|UNION|EXCEPT|INTERSECT|WINDOW)\b[\s\S]*$/i,
1106
+ ""
1107
+ );
1108
+ if (/,|\bJOIN\b|\(/i.test(rest) || rest.trim().split(/\s+/).filter(Boolean).length > 2) return null;
1109
+ const raw = m[1];
1110
+ const first = raw[0];
1111
+ if (first === '"') return raw.slice(1, -1).replaceAll('""', '"');
1112
+ if (first === "`") return raw.slice(1, -1).replaceAll("``", "`");
1113
+ if (first === "[") return raw.slice(1, -1);
1114
+ return raw;
1115
+ }
1116
+ function normalizeDeclaredType3(declared) {
1117
+ if (typeof declared !== "string") return null;
1118
+ const base = declared.trim().toLowerCase().replace(/\(.*$/, "").trim();
1119
+ return base || null;
1120
+ }
1121
+ function inferTypeFromValue3(value) {
1122
+ if (value === null || value === void 0) return "null";
1123
+ if (typeof value === "number") return Number.isInteger(value) ? "integer" : "real";
1124
+ if (Array.isArray(value)) return "blob";
1125
+ return "text";
1126
+ }
1127
+ function firstNonNull3(records, index) {
1128
+ for (const row of records) {
1129
+ const value = row[index];
1130
+ if (value !== null && value !== void 0) return value;
1131
+ }
1132
+ return null;
1133
+ }
1134
+ function closingQuote(sql2, start, close) {
1135
+ let i = start + 1;
1136
+ while (i < sql2.length) {
1137
+ if (sql2[i] === close) {
1138
+ if (sql2[i + 1] === close && close !== "]") {
1139
+ i += 2;
1140
+ continue;
1141
+ }
1142
+ return i + 1;
1143
+ }
1144
+ i += 1;
1145
+ }
1146
+ return sql2.length;
1147
+ }
1148
+ function skipSpace(sql2, from) {
1149
+ let i = from;
1150
+ while (i < sql2.length && /\s/.test(sql2[i])) i += 1;
1151
+ return i;
1152
+ }
1153
+ function routeStatement(sql2, known) {
1154
+ let out = "";
1155
+ let target = null;
1156
+ let i = 0;
1157
+ while (i < sql2.length) {
1158
+ const ch = sql2[i];
1159
+ if (ch === "'") {
1160
+ const end = closingQuote(sql2, i, "'");
1161
+ out += sql2.slice(i, end);
1162
+ i = end;
1163
+ continue;
1164
+ }
1165
+ if (ch === "-" && sql2[i + 1] === "-") {
1166
+ const end = sql2.indexOf("\n", i);
1167
+ const stop = end === -1 ? sql2.length : end;
1168
+ out += sql2.slice(i, stop);
1169
+ i = stop;
1170
+ continue;
1171
+ }
1172
+ if (ch === "/" && sql2[i + 1] === "*") {
1173
+ const end = sql2.indexOf("*/", i + 2);
1174
+ const stop = end === -1 ? sql2.length : end + 2;
1175
+ out += sql2.slice(i, stop);
1176
+ i = stop;
1177
+ continue;
1178
+ }
1179
+ if (ch === '"' || ch === "`" || ch === "[") {
1180
+ const close = ch === "[" ? "]" : ch;
1181
+ const end = closingQuote(sql2, i, close);
1182
+ const raw = sql2.slice(i, end);
1183
+ const inner = ch === "[" ? raw.slice(1, -1) : raw.slice(1, -1).replaceAll(close + close, close);
1184
+ const dot = skipSpace(sql2, end);
1185
+ if (sql2[dot] === "." && known(inner)) {
1186
+ target ??= inner;
1187
+ i = dot + 1;
1188
+ continue;
1189
+ }
1190
+ out += raw;
1191
+ i = end;
1192
+ continue;
1193
+ }
1194
+ out += ch;
1195
+ i += 1;
1196
+ }
1197
+ return { sql: out, target };
1198
+ }
1199
+ async function fetchJson(url, init, unreachable) {
1200
+ let res;
1201
+ try {
1202
+ res = await fetch(url, { ...init, signal: AbortSignal.timeout(6e4) });
1203
+ } catch (err) {
1204
+ throw unreachable(err instanceof Error ? err.message : String(err));
1205
+ }
1206
+ let body = null;
1207
+ try {
1208
+ body = await res.json();
1209
+ } catch {
1210
+ }
1211
+ return { res, body };
1212
+ }
1213
+ var LocalTransport = class {
1214
+ label;
1215
+ base;
1216
+ constructor(devUrl) {
1217
+ this.label = `do:${devUrl}`;
1218
+ this.base = `${devUrl.replace(/\/+$/, "")}${LOCAL_EXPLORER}`;
1219
+ }
1220
+ async call(path, body) {
1221
+ const origin = this.base.replace(LOCAL_EXPLORER, "");
1222
+ const { res, body: parsed } = await fetchJson(
1223
+ `${this.base}${path}`,
1224
+ {
1225
+ method: body === void 0 ? "GET" : "POST",
1226
+ headers: body === void 0 ? void 0 : { "Content-Type": "application/json" },
1227
+ body: body === void 0 ? void 0 : JSON.stringify(body)
1228
+ },
1229
+ (reason) => new DoError(
1230
+ `Cannot reach a wrangler dev server at ${origin} (${reason}). Run \`wrangler dev\` in your Worker project and try again.`,
1231
+ 400
1232
+ )
1233
+ );
1234
+ const envelope = parsed && typeof parsed.success === "boolean" && (parsed.result !== void 0 || Array.isArray(parsed.errors)) ? parsed : null;
1235
+ if (!envelope) {
1236
+ throw new DoError(
1237
+ `${origin} answered (HTTP ${res.status}) but is not a wrangler dev server. Check the port, and that \`wrangler dev\` (wrangler 4.x) is what is running there.`,
1238
+ 400
1239
+ );
1240
+ }
1241
+ if (!res.ok || !envelope.success) {
1242
+ throw new DoError(errorText(envelope.errors) || `wrangler dev responded ${res.status}`, res.ok ? void 0 : 400);
1243
+ }
1244
+ if (envelope.result === void 0) throw new DoError("wrangler dev returned no result");
1245
+ return envelope.result;
1246
+ }
1247
+ async namespaces() {
1248
+ const list = await this.call("/workers/durable_objects/namespaces");
1249
+ return list.map((ns) => ({ id: ns.id, sqlite: ns.use_sqlite !== false }));
1250
+ }
1251
+ objects(namespaceId) {
1252
+ return this.call(`/workers/durable_objects/namespaces/${encodeURIComponent(namespaceId)}/objects`);
1253
+ }
1254
+ async run(target, sql2) {
1255
+ const selector = target.name ? { durable_object_name: target.name } : { durable_object_id: target.id };
1256
+ const results = await this.call(
1257
+ `/workers/durable_objects/namespaces/${encodeURIComponent(target.namespaceId)}/query`,
1258
+ { ...selector, queries: [{ sql: sql2 }] }
1259
+ );
1260
+ const result = results[0];
1261
+ if (!result) throw new DoError("wrangler dev returned no result");
1262
+ return result;
1263
+ }
1264
+ };
1265
+ var RemoteTransport = class {
1266
+ label;
1267
+ base;
1268
+ headers;
1269
+ only;
1270
+ constructor(config) {
1271
+ this.label = `do:${config.accountId}${config.namespaceId ? `/${config.namespaceId}` : ""}`;
1272
+ this.base = `${CLOUDFLARE_API}/accounts/${encodeURIComponent(config.accountId)}/workers/durable_objects/namespaces`;
1273
+ this.headers = { Authorization: `Bearer ${config.apiToken}`, "Content-Type": "application/json" };
1274
+ this.only = config.namespaceId || void 0;
1275
+ }
1276
+ async call(path, body) {
1277
+ const { res, body: parsed } = await fetchJson(
1278
+ `${this.base}${path}`,
1279
+ {
1280
+ method: body === void 0 ? "GET" : "POST",
1281
+ headers: this.headers,
1282
+ body: body === void 0 ? void 0 : JSON.stringify(body)
1283
+ },
1284
+ (reason) => new DoError(`Cannot reach the Cloudflare API: ${reason}`, 400)
1285
+ );
1286
+ if (res.status === 401 || res.status === 403) {
1287
+ throw new DoError(
1288
+ errorText(parsed?.errors) || "Cloudflare refused the API token. It needs the Workers Scripts Read permission on this account.",
1289
+ 401
1290
+ );
1291
+ }
1292
+ if (res.status === 404 || res.status === 405) {
1293
+ throw new DoError(
1294
+ "Cloudflare did not accept this request (HTTP " + res.status + "). The Durable Object query endpoint is unofficial and may have changed; the Data Studio tab in the Cloudflare dashboard still works.",
1295
+ 400
1296
+ );
1297
+ }
1298
+ if (!res.ok || !parsed?.success) {
1299
+ throw new DoError(errorText(parsed?.errors) || `Cloudflare API responded ${res.status}`, 400);
1300
+ }
1301
+ return parsed;
1302
+ }
1303
+ async namespaces() {
1304
+ if (this.only) return [{ id: this.only, sqlite: true }];
1305
+ const body = await this.call("");
1306
+ return (body.result ?? []).map((ns) => ({ id: ns.id, sqlite: ns.use_sqlite !== false }));
1307
+ }
1308
+ /**
1309
+ * Cloudflare's listing returns ids and `hasStoredData`, never names. It also
1310
+ * answers an empty page for small `limit` values, so the parameter is left
1311
+ * off and the default page size is used, following `result_info.cursor`.
1312
+ */
1313
+ async objects(namespaceId) {
1314
+ const out = [];
1315
+ let cursor;
1316
+ do {
1317
+ const query = cursor ? `?cursor=${encodeURIComponent(cursor)}` : "";
1318
+ const body = await this.call(
1319
+ `/${encodeURIComponent(namespaceId)}/objects${query}`
1320
+ );
1321
+ for (const object of body.result ?? []) out.push({ id: object.id });
1322
+ cursor = body.result_info?.cursor || void 0;
1323
+ } while (cursor && out.length < MAX_OBJECTS);
1324
+ return out;
1325
+ }
1326
+ async run(target, sql2) {
1327
+ if (!target.id) {
1328
+ throw new DoError(
1329
+ `"${target.name}" is a name, and deployed Durable Objects can only be opened by id: idFromName() is a one-way hash and Cloudflare does not resolve it here. Pick an object from the schema tree.`,
1330
+ 400
1331
+ );
1332
+ }
1333
+ const body = await this.call(
1334
+ `/${encodeURIComponent(target.namespaceId)}/query`,
1335
+ { durable_object_id: target.id, queries: [{ sql: sql2 }] }
1336
+ );
1337
+ const result = body.result?.results?.[0];
1338
+ if (!result) throw new DoError("Cloudflare returned no result");
1339
+ return result;
1340
+ }
1341
+ };
1342
+ function errorText(errors) {
1343
+ if (!Array.isArray(errors)) return "";
1344
+ return errors.map((e) => typeof e === "string" ? e : String(e?.message ?? "")).filter(Boolean).join("; ").slice(0, 500);
1345
+ }
1346
+ var DoDriver = class {
1347
+ constructor(type, transport) {
1348
+ this.type = type;
1349
+ this.transport = transport;
1350
+ this.label = transport.label;
1351
+ }
1352
+ type;
1353
+ transport;
1354
+ label;
1355
+ /** Database name -> the object it addresses, refreshed by every listing. */
1356
+ targets = /* @__PURE__ */ new Map();
1357
+ /** Declared column types per `database table`, for result-source resolution. */
1358
+ columns = /* @__PURE__ */ new Map();
1359
+ /**
1360
+ * Every object of every SQLite-backed namespace, as app-level database
1361
+ * names. Locally, a `wrangler dev` session registers itself in wrangler's
1362
+ * dev registry, so sessions of other projects show up here too.
1363
+ */
1364
+ async listDatabases() {
1365
+ const namespaces = await this.transport.namespaces();
1366
+ const names = [];
1367
+ this.targets.clear();
1368
+ for (const ns of namespaces) {
1369
+ if (!ns.sqlite) continue;
1370
+ let objects;
1371
+ try {
1372
+ objects = await this.transport.objects(ns.id);
1373
+ } catch {
1374
+ continue;
1375
+ }
1376
+ for (const object of objects) {
1377
+ const ref = object.name || object.id;
1378
+ if (!ref) continue;
1379
+ const database = `${ns.id}/${ref}`;
1380
+ this.targets.set(database, { namespaceId: ns.id, name: object.name, id: object.id });
1381
+ names.push(database);
1382
+ }
1383
+ }
1384
+ return names.sort();
1385
+ }
1386
+ /**
1387
+ * Resolve a database name to the object it addresses. Names are cached by
1388
+ * the last listing; one that was never listed is still usable, so a saved
1389
+ * query tab keeps working — the trailing part is read as an id when it looks
1390
+ * like one and as a name otherwise.
1391
+ */
1392
+ targetFor(database) {
1393
+ const cached2 = this.targets.get(database);
1394
+ if (cached2) return cached2;
1395
+ const slash = database.indexOf("/");
1396
+ if (slash <= 0 || slash === database.length - 1) {
1397
+ throw new DoError(
1398
+ `"${database}" is not a Durable Object. Pick one from the schema tree (they are named "<namespace>/<object>").`,
1399
+ 400
1400
+ );
1401
+ }
1402
+ const namespaceId = database.slice(0, slash);
1403
+ const ref = database.slice(slash + 1);
1404
+ return HEX_ID.test(ref) ? { namespaceId, id: ref } : { namespaceId, name: ref };
1405
+ }
1406
+ async tableColumns(database, target, table) {
1407
+ const key = `${database} ${table}`;
1408
+ const cached2 = this.columns.get(key);
1409
+ if (cached2 !== void 0) return cached2;
1410
+ let out = null;
1411
+ try {
1412
+ const info = await this.transport.run(target, `PRAGMA table_info("${table.replaceAll('"', '""')}")`);
1413
+ const cols = info.columns ?? [];
1414
+ const name = cols.indexOf("name");
1415
+ const type = cols.indexOf("type");
1416
+ const rows = info.rows ?? [];
1417
+ if (rows.length > 0 && name >= 0) {
1418
+ out = new Map(rows.map((r) => [String(r[name]), normalizeDeclaredType3(type >= 0 ? r[type] : null)]));
1419
+ }
1420
+ } catch {
1421
+ out = null;
1422
+ }
1423
+ if (this.columns.size > 200) this.columns.clear();
1424
+ this.columns.set(key, out);
1425
+ return out;
1426
+ }
1427
+ async query(sql2, database) {
1428
+ if (DATABASE_LIST2.test(sql2)) {
1429
+ const names = await this.listDatabases();
1430
+ return {
1431
+ fields: ["seq", "name", "file"].map((name2) => ({
1432
+ name: name2,
1433
+ orgName: name2,
1434
+ type: name2 === "seq" ? "integer" : "text",
1435
+ table: "",
1436
+ orgTable: "",
1437
+ db: "",
1438
+ maxLength: 0
1439
+ })),
1440
+ records: names.map((name2, seq) => [seq, name2, ""]),
1441
+ affectedRows: 0,
1442
+ insertId: 0,
1443
+ isResultQuery: true
1444
+ };
1445
+ }
1446
+ const routed = routeStatement(sql2, (name2) => this.targets.has(name2) || name2.includes("/"));
1447
+ const text = routed.sql;
1448
+ if (TRANSACTION.test(text)) {
1449
+ throw new DoError(
1450
+ "Durable Object storage does not take transaction statements over this API; every request auto-commits."
1451
+ );
1452
+ }
1453
+ const name = routed.target ?? database;
1454
+ if (!name) {
1455
+ throw new DoError(
1456
+ 'No Durable Object selected. Pick one in the schema tree, or qualify the table as "<namespace>/<object>"."<table>".',
1457
+ 400
1458
+ );
1459
+ }
1460
+ const target = this.targetFor(name);
1461
+ if (DDL.test(text)) this.columns.clear();
1462
+ const result = await this.transport.run(target, text);
1463
+ const columns = result.columns ?? [];
1464
+ const records = result.rows ?? [];
1465
+ if (columns.length === 0 && !RESULT_KIND2.test(text)) {
1466
+ return {
1467
+ fields: [],
1468
+ records: [],
1469
+ affectedRows: result.meta?.rows_written ?? 0,
1470
+ insertId: 0,
1471
+ isResultQuery: false
1472
+ };
1473
+ }
1474
+ const source = RESULT_KIND2.test(text) ? singleSourceTable3(text) : null;
1475
+ const known = source ? await this.tableColumns(name, target, source) : null;
1476
+ const fields = columns.map((column, index) => {
1477
+ const fromTable = known?.has(column) ?? false;
1478
+ const type = (fromTable ? known?.get(column) : null) ?? inferTypeFromValue3(firstNonNull3(records, index));
1479
+ return {
1480
+ name: column,
1481
+ orgName: column,
1482
+ type,
1483
+ table: fromTable && source ? source : "",
1484
+ orgTable: fromTable && source ? source : "",
1485
+ db: fromTable ? name : "",
1486
+ maxLength: 0
1487
+ };
1488
+ });
1489
+ return { fields, records, affectedRows: 0, insertId: 0, isResultQuery: true };
1490
+ }
1491
+ async close() {
1492
+ this.targets.clear();
1493
+ this.columns.clear();
1494
+ }
1495
+ };
1496
+ var LOOPBACK = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::1]", "::1", "0.0.0.0"]);
1497
+ function parseDevUrl(raw) {
1498
+ let url;
1499
+ try {
1500
+ url = new URL(raw);
1501
+ } catch {
1502
+ throw new Error(`Not a valid URL: ${raw}`);
1503
+ }
1504
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
1505
+ throw new Error("devUrl must be an http(s) URL");
1506
+ }
1507
+ if (!LOOPBACK.has(url.hostname.toLowerCase())) {
1508
+ throw new Error(
1509
+ "devUrl must point at a wrangler dev server on this machine (localhost). Use the deployed Durable Objects connection for objects that are not local."
1510
+ );
1511
+ }
1512
+ return url.origin;
1513
+ }
1514
+ var doDriverFactory = {
1515
+ async open(config) {
1516
+ const driver = new DoDriver("do", new LocalTransport(config.devUrl));
1517
+ await driver.query("PRAGMA database_list");
1518
+ return driver;
1519
+ },
1520
+ async test(config) {
1521
+ await new DoDriver("do", new LocalTransport(config.devUrl)).query("PRAGMA database_list");
1522
+ },
1523
+ parseConfig(body) {
1524
+ const devUrl = typeof body.devUrl === "string" ? body.devUrl.trim() : "";
1525
+ if (!devUrl) throw new Error("devUrl is required");
1526
+ return { driver: "do", devUrl: parseDevUrl(devUrl) };
1527
+ }
1528
+ };
1529
+ var doRemoteDriverFactory = {
1530
+ async open(config) {
1531
+ const driver = new DoDriver("do-remote", new RemoteTransport(config));
1532
+ await driver.query("PRAGMA database_list");
1533
+ return driver;
1534
+ },
1535
+ async test(config) {
1536
+ await new DoDriver("do-remote", new RemoteTransport(config)).query("PRAGMA database_list");
1537
+ },
1538
+ parseConfig(body) {
1539
+ const accountId = typeof body.accountId === "string" ? body.accountId.trim() : "";
1540
+ const apiToken = typeof body.apiToken === "string" ? body.apiToken.trim() : "";
1541
+ const namespaceId = typeof body.namespaceId === "string" ? body.namespaceId.trim() : "";
1542
+ if (!accountId || !apiToken) throw new Error("accountId and apiToken are required");
1543
+ return { driver: "do-remote", accountId, apiToken, ...namespaceId ? { namespaceId } : {} };
1544
+ }
1545
+ };
1546
+
1031
1547
  // src/ssh-tunnel.ts
1032
1548
  import { createServer } from "net";
1033
1549
  import { mkdir, readFile as readFile2, appendFile } from "fs/promises";
@@ -1551,23 +2067,28 @@ var factories = {
1551
2067
  sqlite: sqliteDriverFactory,
1552
2068
  mssql: mssqlDriverFactory,
1553
2069
  oracle: oracleDriverFactory,
1554
- d1: d1DriverFactory
2070
+ d1: d1DriverFactory,
2071
+ do: doDriverFactory,
2072
+ "do-remote": doRemoteDriverFactory
1555
2073
  };
1556
2074
  function isDriverType(value) {
1557
2075
  return typeof value === "string" && value in factories;
1558
2076
  }
2077
+ function isDirectDriver(config) {
2078
+ return config.driver === "sqlite" || config.driver === "d1" || config.driver === "do" || config.driver === "do-remote";
2079
+ }
1559
2080
  function parseDriverConfig(body) {
1560
2081
  const driver = body.driver ?? "mysql";
1561
2082
  if (!isDriverType(driver)) {
1562
2083
  throw new Error(`Unsupported driver: ${String(driver)}`);
1563
2084
  }
1564
2085
  const config = factories[driver].parseConfig(body);
1565
- if (config.driver === "sqlite" || config.driver === "d1") return config;
2086
+ if (isDirectDriver(config)) return config;
1566
2087
  const ssh = parseSshConfig(body.ssh);
1567
2088
  return ssh ? { ...config, ssh } : config;
1568
2089
  }
1569
2090
  async function withTunnel(config, run) {
1570
- if (config.driver === "sqlite" || config.driver === "d1" || !config.ssh) return run(config, void 0);
2091
+ if (isDirectDriver(config) || !config.ssh) return run(config, void 0);
1571
2092
  const tunnel = await openSshTunnel(config.ssh, { host: config.host, port: config.port });
1572
2093
  recordUsage("ssh_tunnel");
1573
2094
  try {
@@ -1595,7 +2116,7 @@ function tunneled(driver, tunnel, label) {
1595
2116
  function openDriver(config) {
1596
2117
  return withTunnel(config, async (direct, tunnel) => {
1597
2118
  const driver = await openDirect(direct);
1598
- if (!tunnel || config.driver === "sqlite" || config.driver === "d1" || !config.ssh) return driver;
2119
+ if (!tunnel || isDirectDriver(config) || !config.ssh) return driver;
1599
2120
  const label = `${config.user}@${config.host}:${config.port} via ssh ${config.ssh.user}@${config.ssh.host}:${config.ssh.port}`;
1600
2121
  return tunneled(driver, tunnel, label);
1601
2122
  });
@@ -1623,6 +2144,10 @@ function openDirect(config) {
1623
2144
  return factories.oracle.open(config);
1624
2145
  case "d1":
1625
2146
  return factories.d1.open(config);
2147
+ case "do":
2148
+ return factories.do.open(config);
2149
+ case "do-remote":
2150
+ return factories["do-remote"].open(config);
1626
2151
  }
1627
2152
  }
1628
2153
  function testDirect(config) {
@@ -1639,6 +2164,10 @@ function testDirect(config) {
1639
2164
  return factories.oracle.test(config);
1640
2165
  case "d1":
1641
2166
  return factories.d1.test(config);
2167
+ case "do":
2168
+ return factories.do.test(config);
2169
+ case "do-remote":
2170
+ return factories["do-remote"].test(config);
1642
2171
  }
1643
2172
  }
1644
2173
 
@@ -1886,7 +2415,7 @@ async function updateNotice(current) {
1886
2415
 
1887
2416
  // src/server.ts
1888
2417
  import express from "express";
1889
- import { createHash as createHash2, timingSafeEqual as timingSafeEqual2 } from "crypto";
2418
+ import { createHash as createHash2, randomUUID as randomUUID2, timingSafeEqual as timingSafeEqual2 } from "crypto";
1890
2419
 
1891
2420
  // src/tunneling.ts
1892
2421
  import { randomUUID } from "crypto";
@@ -2116,6 +2645,19 @@ var ConnectionManager = class {
2116
2645
  throw new ConnectionLostError(`Connection lost: ${err instanceof Error ? err.message : String(err)}`);
2117
2646
  }
2118
2647
  }
2648
+ /**
2649
+ * Interrupt whatever is running on a connection. Best effort: an engine with
2650
+ * no way to interrupt (better-sqlite3 blocks the thread) reports false rather
2651
+ * than claiming a cancel that did not happen. The connection stays open —
2652
+ * the point is to free it, not to drop it.
2653
+ */
2654
+ async cancel(connectionId) {
2655
+ const driver = this.connections.get(connectionId);
2656
+ if (!driver) throw new Error(`Connection not found: ${connectionId}`);
2657
+ if (!driver.cancel) return false;
2658
+ await driver.cancel();
2659
+ return true;
2660
+ }
2119
2661
  /**
2120
2662
  * Close and remove a connection.
2121
2663
  */
@@ -2253,7 +2795,9 @@ function createAgent(options) {
2253
2795
  const allowedOrigins = new Set(options.allowedOrigins.map((o) => o.replace(/\/+$/, "")));
2254
2796
  const allowedHosts = options.allowedHosts ? new Set(options.allowedHosts.map((h) => h.toLowerCase())) : null;
2255
2797
  const throttle = new AuthThrottle();
2798
+ const pairThrottle = new AuthThrottle();
2256
2799
  let pairing = options.pairingCode;
2800
+ let lastPairOffer = 0;
2257
2801
  app.set("env", "production");
2258
2802
  app.disable("x-powered-by");
2259
2803
  app.disable("etag");
@@ -2288,6 +2832,19 @@ function createAgent(options) {
2288
2832
  });
2289
2833
  app.use(accessControl(allowedOrigins));
2290
2834
  if (options.appDir) serveApp(app, options.appDir);
2835
+ const PAIR_OFFER_INTERVAL_MS = 6e4;
2836
+ function offerPairing(req) {
2837
+ if (!options.onUnpaired) return false;
2838
+ const origin = req.headers.origin;
2839
+ if (typeof origin !== "string" || !allowedOrigins.has(origin.replace(/\/+$/, ""))) return false;
2840
+ const now = Date.now();
2841
+ if (pairing && now <= pairing.expiresAt) return false;
2842
+ if (now - lastPairOffer < PAIR_OFFER_INTERVAL_MS) return false;
2843
+ lastPairOffer = now;
2844
+ pairing = { code: randomUUID2(), expiresAt: now + 10 * 60 * 1e3 };
2845
+ options.onUnpaired(pairing, origin);
2846
+ return true;
2847
+ }
2291
2848
  app.get("/health", (req, res) => {
2292
2849
  const body = {
2293
2850
  success: true,
@@ -2300,23 +2857,21 @@ function createAgent(options) {
2300
2857
  if (options.install) body.install = options.install;
2301
2858
  if (options.gateway) body.gateway = { name: options.gateway.name, profiles: options.gateway.profiles.length };
2302
2859
  if (req.headers.authorization !== void 0) {
2303
- if (throttle.locked(peerOf(req))) {
2304
- fail(res, 429, "Too many failed authentication attempts; try again shortly");
2305
- return;
2306
- }
2307
2860
  body.authorized = authorized(req);
2308
2861
  }
2862
+ if (body.authorized !== true) void offerPairing(req);
2309
2863
  res.json(body);
2310
2864
  });
2311
2865
  app.post("/pair", express.json({ limit: "1kb" }), (req, res) => {
2312
- if (throttle.locked(peerOf(req))) {
2313
- fail(res, 429, "Too many failed authentication attempts; try again shortly");
2866
+ if (pairThrottle.locked(peerOf(req))) {
2867
+ fail(res, 429, "Too many failed pairing attempts; try again shortly");
2314
2868
  return;
2315
2869
  }
2316
2870
  const code = typeof req.body?.code === "string" ? req.body.code : "";
2317
2871
  if (!pairing || Date.now() > pairing.expiresAt || !code || !secretEquals(code, pairing.code)) {
2318
- throttle.fail(peerOf(req));
2319
- fail(res, 401, "Pairing code is invalid or has expired; restart the agent for a new link");
2872
+ pairThrottle.fail(peerOf(req));
2873
+ const printed = offerPairing(req);
2874
+ fail(res, 401, printed ? "Pairing code is invalid or has expired; a fresh link has been printed in the agent's terminal" : "Pairing code is invalid or has expired; open the pairing link shown in the agent's terminal");
2320
2875
  return;
2321
2876
  }
2322
2877
  pairing = void 0;
@@ -2406,6 +2961,24 @@ function createAgent(options) {
2406
2961
  await flushUsage();
2407
2962
  res.json(await usageSnapshot());
2408
2963
  });
2964
+ app.post("/cancel", async (req, res) => {
2965
+ try {
2966
+ const connectionId = req.body?.connectionId;
2967
+ if (typeof connectionId !== "string" || !connectionId) {
2968
+ fail(res, 400, "connectionId is required");
2969
+ return;
2970
+ }
2971
+ const cancelled = await manager.cancel(connectionId);
2972
+ const body = {
2973
+ success: true,
2974
+ cancelled,
2975
+ message: cancelled ? "Cancel sent" : "This engine cannot interrupt a running statement"
2976
+ };
2977
+ res.json(body);
2978
+ } catch (err) {
2979
+ fail(res, errorStatus(err), errorMessage(err));
2980
+ }
2981
+ });
2409
2982
  app.post("/disconnect", async (req, res) => {
2410
2983
  try {
2411
2984
  const { connectionId } = req.body ?? {};
@@ -2633,7 +3206,7 @@ async function main() {
2633
3206
  const port = Number(flags.port ?? process.env.PLANCHE_PORT) || 8888;
2634
3207
  const addr = flags.addr ?? process.env.PLANCHE_ADDR ?? "127.0.0.1";
2635
3208
  const tokenFromEnv = !flags.token && !!process.env.PLANCHE_AUTH_TOKEN;
2636
- const authToken = flags.token ?? process.env.PLANCHE_AUTH_TOKEN ?? randomUUID2();
3209
+ const authToken = flags.token ?? process.env.PLANCHE_AUTH_TOKEN ?? randomUUID3();
2637
3210
  const gatewayFile = flags.gateway ?? process.env.PLANCHE_GATEWAY;
2638
3211
  let gateway;
2639
3212
  if (gatewayFile) {
@@ -2684,9 +3257,11 @@ async function main() {
2684
3257
  const devApp = LOOPBACK_ADDRS.has(new URL(appUrl).hostname);
2685
3258
  const allowedOrigins = [appOrigin, ...devApp ? DEV_ORIGINS : [], ...extraOrigins];
2686
3259
  if (serveApp2) allowedOrigins.push(`http://127.0.0.1:${port}`, `http://localhost:${port}`, `http://[::1]:${port}`);
2687
- const pairingCode = { code: randomUUID2(), expiresAt: Date.now() + 10 * 60 * 1e3 };
3260
+ const pairingCode = { code: randomUUID3(), expiresAt: Date.now() + 10 * 60 * 1e3 };
2688
3261
  const listenAddr = addr.replace(/^\[(.*)\]$/, "$1");
2689
3262
  const agentUrl = `http://${listenAddr.includes(":") ? `[${listenAddr}]` : listenAddr}:${port}`;
3263
+ const divider = "-".repeat(72);
3264
+ const pairLink = (code, appBase) => serveApp2 ? `${appBase}/#pair=${encodeURIComponent(code)}` : `${appBase}/#agent=${encodeURIComponent(agentUrl)}&pair=${encodeURIComponent(code)}`;
2690
3265
  const { app, manager } = createAgent({
2691
3266
  authToken,
2692
3267
  allowedOrigins,
@@ -2698,17 +3273,30 @@ async function main() {
2698
3273
  allowedHosts: loopback ? ["127.0.0.1", "localhost", "[::1]", `127.0.0.1:${port}`, `localhost:${port}`, `[::1]:${port}`] : void 0,
2699
3274
  pairingCode,
2700
3275
  gateway,
2701
- appDir
3276
+ appDir,
3277
+ // An app that cannot authenticate gets a fresh link on this terminal
3278
+ // rather than needing the agent restarted. The link is built from the
3279
+ // origin the request came from, so it points at the app that is really
3280
+ // running — a dev server bumped to another port included.
3281
+ onUnpaired: (fresh, appOrigin2) => {
3282
+ const url = pairLink(fresh.code, serveApp2 ? publicUrl || appOrigin2 : appOrigin2);
3283
+ console.log(divider);
3284
+ console.log(`The app at ${appOrigin2} is not paired with this agent.`);
3285
+ console.log(`Pair it with (valid for 10 minutes, single use):
3286
+
3287
+ ${url}
3288
+ `);
3289
+ console.log(divider);
3290
+ }
2702
3291
  });
2703
- const pairUrl = serveApp2 ? `${publicUrl || agentUrl}/#pair=${encodeURIComponent(pairingCode.code)}` : `${appUrl}/#agent=${encodeURIComponent(agentUrl)}&pair=${encodeURIComponent(pairingCode.code)}`;
3292
+ const pairUrl = pairLink(pairingCode.code, serveApp2 ? publicUrl || agentUrl : appUrl);
2704
3293
  if (offlineFallback) {
2705
3294
  console.log(`${DEFAULT_APP_URL === hostedAppUrl ? "planche.dev" : hostedAppUrl} is unreachable \u2014 serving the app bundled with this agent (v${VERSION}) instead.`);
2706
3295
  }
2707
3296
  const server = app.listen(port, listenAddr, () => {
2708
- const line = "-".repeat(72);
2709
- console.log(line);
3297
+ console.log(divider);
2710
3298
  console.log(`Planche agent ${VERSION} listening on ${agentUrl}`);
2711
- console.log(line);
3299
+ console.log(divider);
2712
3300
  console.log(`Open the app with (link valid for 10 minutes, single use):
2713
3301
 
2714
3302
  ${pairUrl}
@@ -2719,7 +3307,7 @@ async function main() {
2719
3307
  if (!loopback) {
2720
3308
  console.error(`WARNING: bound to ${addr} without TLS \u2014 anyone who can reach this port and knows the token can use your databases, and anyone on the path can read the traffic.`);
2721
3309
  }
2722
- console.log(line);
3310
+ console.log(divider);
2723
3311
  if (shouldOpen) openInBrowser(pairUrl);
2724
3312
  void updateNotice(VERSION).then((notice) => {
2725
3313
  if (notice) console.log(notice);