planche 0.8.3 → 0.8.5

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 (133) hide show
  1. package/app/assets/1E6FYbv2.js +1 -0
  2. package/app/assets/{postgresql-erqhE-5Q.js → 5E2RYjEs.js} +1 -1
  3. package/app/assets/B-Mrqvrb.css +2 -0
  4. package/app/assets/B3LyCSVJ.js +1 -0
  5. package/app/assets/{DownloadToCSVDialog-Bc0PDZHm.js → B3uI9Ppb.js} +2 -2
  6. package/app/assets/B5OocgEJ.js +1 -0
  7. package/app/assets/B8xQ2o-X.js +1 -0
  8. package/app/assets/BBSxV90W.js +1 -0
  9. package/app/assets/BGOnv2_O.js +1 -0
  10. package/app/assets/BIjF9ulh.js +1 -0
  11. package/app/assets/BM5G_YA7.js +1 -0
  12. package/app/assets/{ja-C7vEFXh8.js → BXX-BtBa.js} +1 -1
  13. package/app/assets/{tunneling-B7lL9I14.js → BdPEUnLq.js} +1 -1
  14. package/app/assets/Bpg8LzYe.js +2 -0
  15. package/app/assets/BunrDTHn.js +1 -0
  16. package/app/assets/{ko-1oXH_x0o.js → BziT-o8V.js} +1 -1
  17. package/app/assets/{dbms-BwnatOMe.js → C1RY7wUM.js} +13 -13
  18. package/app/assets/C3bMAsUp.js +1 -0
  19. package/app/assets/C4il2fft.js +1 -0
  20. package/app/assets/CChaTOPr.js +1 -0
  21. package/app/assets/CKOS5Y-e.js +1 -0
  22. package/app/assets/{storage-worker-DEIYmjYK.js → CM41Cs80.js} +3 -3
  23. package/app/assets/COIa4uoZ.js +1 -0
  24. package/app/assets/{de-EBRzfz-s.js → CWY0kZPK.js} +1 -1
  25. package/app/assets/CZQ1r4zz.js +41 -0
  26. package/app/assets/{error-hints-BsG7qe62.js → CfsL8_WC.js} +3 -3
  27. package/app/assets/CkgLFvM5.js +1 -0
  28. package/app/assets/ClyAuDY9.js +2 -0
  29. package/app/assets/CmJ3siBN.js +1 -0
  30. package/app/assets/Cw_LYT6k.js +1 -0
  31. package/app/assets/Cx_chT_j.js +1 -0
  32. package/app/assets/D-RWduy5.js +1 -0
  33. package/app/assets/{index-Tw24-4of.js → D7Ze5md9.js} +176 -176
  34. package/app/assets/{zh-B-LYJHDx.js → D8nrOvmh.js} +1 -1
  35. package/app/assets/DAFA-E1-.js +1 -0
  36. package/app/assets/{dist-Roudme38.js → DD58tsa9.js} +2 -2
  37. package/app/assets/DFNc4ghA.js +1 -0
  38. package/app/assets/DGD5v9rw.js +1 -0
  39. package/app/assets/{mariadb-BT1adDMU.js → DOeLCbYG.js} +1 -1
  40. package/app/assets/DTxGpkJp.js +1 -0
  41. package/app/assets/DZH-tLW-.js +1 -0
  42. package/app/assets/D_2UqK4k.js +1 -0
  43. package/app/assets/DbRMSZEW.js +1 -0
  44. package/app/assets/Dd9Cr3e8.js +1 -0
  45. package/app/assets/DeB5sJfy.js +1 -0
  46. package/app/assets/Dh3YJ3G1.js +1 -0
  47. package/app/assets/DiaA_DOW.js +1 -0
  48. package/app/assets/DlKZCiMg.js +2 -0
  49. package/app/assets/{sqlite3-worker1-CVKz6JuG.js → DlWwFXlb.js} +1 -1
  50. package/app/assets/DmejZTwI.js +1 -0
  51. package/app/assets/{SchemaDiffDialog-DjuZiSux.js → DnjY8W6t.js} +1 -1
  52. package/app/assets/DxuXBECK.js +1 -0
  53. package/app/assets/FnB_uI57.js +1 -0
  54. package/app/assets/IzH8qyLi.js +1 -0
  55. package/app/assets/NY_tAtcY.js +1 -0
  56. package/app/assets/QBd1UkiI.js +1 -0
  57. package/app/assets/{mysql-BEg8tI_S.js → R93pYE1w.js} +1 -1
  58. package/app/assets/RIOwEE7M.js +1 -0
  59. package/app/assets/S4Oofhu9.js +1 -0
  60. package/app/assets/XzPc-eVf.js +1 -0
  61. package/app/assets/cc05Z2oj.js +1 -0
  62. package/app/assets/dxLVUFtU.js +1 -0
  63. package/app/assets/eFSiW0Lh.js +1 -0
  64. package/app/assets/{transactsql-BVCsopZt.js → eRZ39OKr.js} +1 -1
  65. package/app/assets/fCzQKDqY.js +1 -0
  66. package/app/assets/{SchemaToHTMLDialog-NzNj1jXs.js → og5dTmtH.js} +1 -1
  67. package/app/assets/{RowFormDialog-pQYmxMrm.js → qbvehL2h.js} +1 -1
  68. package/app/assets/rcI4Fmt0.js +1 -0
  69. package/app/assets/{fr-DERxGVO3.js → u80owQsI.js} +1 -1
  70. package/app/index.html +22 -22
  71. package/dist/chunk-2Z2E3JGN.js +3733 -0
  72. package/dist/embed.js +21 -0
  73. package/dist/index.js +210 -3920
  74. package/package.json +4 -1
  75. package/app/assets/AboutDialog-DuqxA9a9.js +0 -1
  76. package/app/assets/AiSettingsDialog-CgGNd3Oc.js +0 -1
  77. package/app/assets/BigInteger-BlV0QR66.js +0 -1
  78. package/app/assets/ConnectDialog-H3vlQRP_.js +0 -1
  79. package/app/assets/CopyDatabaseDialog-CVoYRBig.js +0 -1
  80. package/app/assets/CopyTableDialog-DC1kxjD2.js +0 -1
  81. package/app/assets/CreateDatabaseDialog-BYwKAc5-.js +0 -1
  82. package/app/assets/CreateSchemaDialog-BhPRvUT-.js +0 -1
  83. package/app/assets/DataDiffDialog-CXMnUq1B.js +0 -1
  84. package/app/assets/EditSchemaDialog-B4h1xWEz.js +0 -2
  85. package/app/assets/FindReplaceDialog-BkDPXuox.js +0 -1
  86. package/app/assets/FlushDialog-BlPJ6BTk.js +0 -1
  87. package/app/assets/GeneralSettingsDialog-ByNvtugk.js +0 -1
  88. package/app/assets/GroupByDialog-B593U6zZ.js +0 -1
  89. package/app/assets/LicenseDialog-3TWjXiJM.js +0 -1
  90. package/app/assets/LinkColumnDialog-D3ygO1ut.js +0 -1
  91. package/app/assets/LocalHistoryDialog-DPgqYYkG.js +0 -1
  92. package/app/assets/McpSetupDialog-DOS0fPHQ.js +0 -1
  93. package/app/assets/ProcessListDialog-DNqFCxy7.js +0 -1
  94. package/app/assets/QuerySettingsDialog-BjD8gLmZ.js +0 -1
  95. package/app/assets/QuickCommandPalette-06i8D2JM.js +0 -1
  96. package/app/assets/SafetySettingsDialog-a0Y03qEt.js +0 -1
  97. package/app/assets/StatusDialog-DVKcDDpl.js +0 -1
  98. package/app/assets/SyncDialog-CTyRxpKa.js +0 -1
  99. package/app/assets/TermsDialog-oo1J6DXC.js +0 -1
  100. package/app/assets/UserAddDialog-CtZpkft3.js +0 -1
  101. package/app/assets/UserManagerDialog-Dk1mzBsu.js +0 -1
  102. package/app/assets/VariablesDialog-Blu74K9P.js +0 -1
  103. package/app/assets/app-storage-C_1RLCyI.js +0 -1
  104. package/app/assets/button-B2EsmMJs.js +0 -1
  105. package/app/assets/chevron-up-CBnVjNE1.js +0 -1
  106. package/app/assets/connection-store-B85kbJpB.js +0 -1
  107. package/app/assets/database-BSXLJRzV.js +0 -1
  108. package/app/assets/dialog-FBpgH0QJ.js +0 -1
  109. package/app/assets/dist-BYhFWvmC.js +0 -41
  110. package/app/assets/editor-registry-ukUyoekH.js +0 -1
  111. package/app/assets/engine-DjP-Sw-S.js +0 -1
  112. package/app/assets/i18n-C0aLwQ_k.js +0 -2
  113. package/app/assets/index-dwv2QL4R.css +0 -2
  114. package/app/assets/pro-upsell-DhbKIX46.js +0 -1
  115. package/app/assets/pro-usage-BxwRZwtM.js +0 -1
  116. package/app/assets/query-execution-BeDEQ-3M.js +0 -2
  117. package/app/assets/query-store-BqBiylGA.js +0 -1
  118. package/app/assets/schema-nodes-MgIiKfpG.js +0 -1
  119. package/app/assets/select-DEqoX1sU.js +0 -1
  120. package/app/assets/settings-controls-BepR6gzL.js +0 -1
  121. package/app/assets/settings-store-DzQ3BP5e.js +0 -1
  122. package/app/assets/ui-store-5IssrgmG.js +0 -1
  123. package/app/assets/user-store-DOy2VMsT.js +0 -1
  124. /package/app/assets/{sqlite3-BVKGSWc-.wasm → BVKGSWc-.wasm} +0 -0
  125. /package/app/assets/{editor.worker-BboMRUer.js → BboMRUer.js} +0 -0
  126. /package/app/assets/{hmr-singleton-Bit0Kfog.js → Bit0Kfog.js} +0 -0
  127. /package/app/assets/{table-definition-Bs5VSn0X.js → Bs5VSn0X.js} +0 -0
  128. /package/app/assets/{file-name-CnYBZPf9.js → CnYBZPf9.js} +0 -0
  129. /package/app/assets/{sqlite3-opfs-async-proxy-D_xnb1D8.js → D_xnb1D8.js} +0 -0
  130. /package/app/assets/{sql-NEE52Syq.js → NEE52Syq.js} +0 -0
  131. /package/app/assets/{rolldown-runtime-hePW80VL.js → hePW80VL.js} +0 -0
  132. /package/app/assets/{contract-neLPq0_M.js → neLPq0_M.js} +0 -0
  133. /package/app/assets/{codicon-ngg6Pgfi.ttf → ngg6Pgfi.ttf} +0 -0
package/dist/index.js CHANGED
@@ -1,3952 +1,242 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ configureCliBridge,
4
+ createAgent,
5
+ ensureMcpToken,
6
+ flushUsage,
7
+ forgetMcpToken,
8
+ loadGatewayConfig,
9
+ mcpTokenPath
10
+ } from "./chunk-2Z2E3JGN.js";
2
11
 
3
12
  // src/index.ts
4
- import { randomUUID as randomUUID3 } from "crypto";
5
- import { createRequire as createRequire2 } from "module";
6
- import { spawn as spawn2 } from "child_process";
13
+ import { randomUUID } from "crypto";
14
+ import { createRequire } from "module";
15
+ import { spawn } from "child_process";
7
16
  import { parseArgs } from "util";
17
+ import { existsSync as existsSync2 } from "fs";
18
+ import { join as join2, resolve as resolve2 } from "path";
19
+ import { fileURLToPath as fileURLToPath2 } from "url";
8
20
 
9
- // src/gateway.ts
10
- import { readFileSync } from "fs";
11
- import { resolve as resolve2 } from "path";
12
-
13
- // src/drivers/mysql.ts
14
- import mysql from "mysql2/promise";
15
- var MYSQL_TYPES = {
16
- 0: "decimal",
17
- 1: "tinyint",
18
- 2: "smallint",
19
- 3: "int",
20
- 4: "float",
21
- 5: "double",
22
- 6: "null",
23
- 7: "timestamp",
24
- 8: "bigint",
25
- 9: "mediumint",
26
- 10: "date",
27
- 11: "time",
28
- 12: "datetime",
29
- 13: "year",
30
- 14: "newdate",
31
- 15: "varchar",
32
- 16: "bit",
33
- 245: "json",
34
- 246: "newdecimal",
35
- 247: "enum",
36
- 248: "set",
37
- 249: "tinyblob",
38
- 250: "mediumblob",
39
- 251: "longblob",
40
- 252: "blob",
41
- 253: "varstring",
42
- 254: "string",
43
- 255: "geometry"
44
- };
45
- function getTypeName(typeId) {
46
- return MYSQL_TYPES[typeId] ?? "unknown";
47
- }
48
- function toConnectionOptions(config) {
49
- return {
50
- host: config.host,
51
- port: config.port,
52
- user: config.user,
53
- password: config.password,
54
- database: config.database || void 0,
55
- dateStrings: true,
56
- charset: config.charset || "utf8mb4",
57
- ssl: config.ssl ? { rejectUnauthorized: config.sslVerify === true } : void 0
58
- };
59
- }
60
- var USE_STATEMENT = /^\s*USE\s/i;
61
- var MySqlDriver = class {
62
- constructor(connection, config) {
63
- this.connection = connection;
64
- this.config = config;
65
- this.label = `${config.user}@${config.host}:${config.port}`;
66
- this.currentDatabase = config.database ?? null;
67
- }
68
- connection;
69
- config;
70
- type = "mysql";
71
- label;
72
- /**
73
- * Default schema the session is currently in, or null when unknown (after
74
- * the client ran its own `USE`). Tracked so the schema is switched with a
75
- * plain `USE` only when it actually changes: `changeUser()` would reset the
76
- * whole session on every call, silently rolling back open transactions and
77
- * dropping temporary tables and user variables.
78
- */
79
- currentDatabase;
80
- async query(sql2, database) {
81
- if (database && database !== this.currentDatabase) {
82
- await this.connection.query(`USE ${mysql.escapeId(database)}`);
83
- this.currentDatabase = database;
84
- }
85
- const [results, fields] = await this.connection.query(sql2);
86
- if (USE_STATEMENT.test(sql2)) this.currentDatabase = null;
87
- if (Array.isArray(results) && fields) {
88
- const fieldPackets = fields;
89
- const mappedFields = fieldPackets.map((field) => ({
90
- name: field.name,
91
- orgName: field.orgName ?? field.name,
92
- type: getTypeName(field.type ?? 253),
93
- table: field.table ?? "",
94
- orgTable: field.orgTable ?? "",
95
- db: field.db ?? "",
96
- maxLength: field.length ?? 0
97
- }));
98
- const records = results.map(
99
- (row) => fieldPackets.map((field) => row[field.name])
100
- );
101
- return { fields: mappedFields, records, affectedRows: 0, insertId: 0, isResultQuery: true };
102
- }
103
- const header = results;
104
- return {
105
- fields: [],
106
- records: [],
107
- affectedRows: header.affectedRows ?? 0,
108
- insertId: header.insertId ?? 0,
109
- isResultQuery: false
110
- };
111
- }
112
- async setReadOnly() {
113
- await this.connection.query("SET SESSION TRANSACTION READ ONLY");
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
- }
130
- async close() {
131
- await this.connection.end();
132
- }
133
- };
134
- var mysqlDriverFactory = {
135
- async open(config) {
136
- const connection = await mysql.createConnection(toConnectionOptions(config));
137
- connection.on("error", (err) => console.error("[mysql] Connection error:", err.message));
138
- return new MySqlDriver(connection, config);
139
- },
140
- async test(config) {
141
- const connection = await mysql.createConnection({
142
- ...toConnectionOptions(config),
143
- database: void 0
144
- });
145
- await connection.end();
146
- },
147
- parseConfig(body) {
148
- const host = typeof body.host === "string" ? body.host.trim() : "";
149
- const user = typeof body.user === "string" ? body.user : "";
150
- if (!host || !user) {
151
- throw new Error("host and user are required");
152
- }
153
- return {
154
- driver: "mysql",
155
- host,
156
- port: Number(body.port) || 3306,
157
- user,
158
- password: typeof body.password === "string" ? body.password : "",
159
- database: typeof body.database === "string" && body.database ? body.database : void 0,
160
- charset: typeof body.charset === "string" && body.charset ? body.charset : void 0,
161
- ssl: body.ssl === true,
162
- sslVerify: body.sslVerify === true
163
- };
164
- }
165
- };
166
-
167
- // src/drivers/postgres.ts
168
- import pg from "pg";
169
- var TEXT_OIDS = [
170
- 20,
171
- // int8
172
- 1700,
173
- // numeric
174
- 1082,
175
- // date
176
- 1114,
177
- // timestamp
178
- 1184,
179
- // timestamptz
180
- 1083,
181
- // time
182
- 1266,
183
- // timetz
184
- 1186
185
- // interval
186
- ];
187
- var types = new pg.TypeOverrides();
188
- for (const oid of TEXT_OIDS) types.setTypeParser(oid, (value) => value);
189
- var BUILTIN_TYPES = {
190
- 16: "bool",
191
- 17: "bytea",
192
- 18: "char",
193
- 19: "name",
194
- 20: "int8",
195
- 21: "int2",
196
- 23: "int4",
197
- 25: "text",
198
- 26: "oid",
199
- 114: "json",
200
- 142: "xml",
201
- 700: "float4",
202
- 701: "float8",
203
- 1042: "bpchar",
204
- 1043: "varchar",
205
- 1082: "date",
206
- 1083: "time",
207
- 1114: "timestamp",
208
- 1184: "timestamptz",
209
- 1186: "interval",
210
- 1266: "timetz",
211
- 1700: "numeric",
212
- 2950: "uuid",
213
- 3802: "jsonb",
214
- 1560: "bit",
215
- 1562: "varbit",
216
- 600: "point",
217
- 869: "inet",
218
- 650: "cidr",
219
- 829: "macaddr",
220
- 1007: "_int4",
221
- 1009: "_text"
222
- };
223
- function toClientConfig(config) {
224
- return {
225
- host: config.host,
226
- port: config.port,
227
- user: config.user,
228
- password: config.password,
229
- database: config.database,
230
- ssl: config.ssl ? { rejectUnauthorized: config.sslVerify === true } : void 0,
231
- types
232
- };
233
- }
234
- var SEARCH_PATH_STATEMENT = /^\s*SET\s+(SESSION\s+|LOCAL\s+)?search_path\b/i;
235
- var quoteIdent = (name) => `"${name.replaceAll('"', '""')}"`;
236
- var PostgresDriver = class {
237
- constructor(client2, config) {
238
- this.client = client2;
239
- this.config = config;
240
- this.label = `${config.user}@${config.host}:${config.port}/${config.database}`;
241
- }
242
- client;
243
- config;
244
- type = "postgres";
245
- label;
246
- /**
247
- * Schema currently first in `search_path`, or null when unknown (after the
248
- * client set it itself). The app's "database" for a Postgres connection is
249
- * a schema of the bound database, switched with `SET search_path`.
250
- */
251
- currentSchema = null;
252
- typeNames = { ...BUILTIN_TYPES };
253
- /**
254
- * Result fields name their source only by OID (`tableID` / `columnID`);
255
- * the frontend needs schema/table/column names to decide whether a grid is
256
- * editable and to build UPDATEs, so they are resolved here and cached.
257
- */
258
- relations = /* @__PURE__ */ new Map();
259
- async loadTypeNames() {
260
- try {
261
- const res = await this.client.query(
262
- "SELECT oid, typname FROM pg_type WHERE typtype IN ('b', 'e', 'd', 'r')"
263
- );
264
- for (const row of res.rows) this.typeNames[Number(row.oid)] = row.typname;
265
- } catch {
266
- }
267
- }
268
- async resolveRelations(tableIds) {
269
- const missing = [...new Set(tableIds)].filter((id) => id > 0 && !this.relations.has(id));
270
- if (missing.length === 0) return;
271
- try {
272
- const res = await this.client.query(
273
- `SELECT c.oid, n.nspname, c.relname, a.attnum, a.attname
274
- FROM pg_class c
275
- JOIN pg_namespace n ON n.oid = c.relnamespace
276
- JOIN pg_attribute a ON a.attrelid = c.oid AND a.attnum > 0 AND NOT a.attisdropped
277
- WHERE c.oid = ANY($1::oid[])`,
278
- [missing]
279
- );
280
- for (const row of res.rows) {
281
- const oid = Number(row.oid);
282
- let rel = this.relations.get(oid);
283
- if (!rel) {
284
- rel = { schema: row.nspname, table: row.relname, columns: /* @__PURE__ */ new Map() };
285
- this.relations.set(oid, rel);
286
- }
287
- rel.columns.set(Number(row.attnum), row.attname);
288
- }
289
- } catch {
290
- }
291
- }
292
- async query(sql2, database) {
293
- if (database && database !== this.currentSchema) {
294
- await this.client.query(`SET search_path TO ${quoteIdent(database)}, public`);
295
- this.currentSchema = database;
296
- }
297
- const result = await this.client.query({ text: sql2, rowMode: "array" });
298
- if (SEARCH_PATH_STATEMENT.test(sql2)) this.currentSchema = null;
299
- const results = Array.isArray(result) ? result : [result];
300
- const last = [...results].reverse().find((r) => r.fields.length > 0) ?? results[results.length - 1];
301
- if (last.fields.length > 0) {
302
- await this.resolveRelations(last.fields.map((f) => f.tableID));
303
- const fields = last.fields.map((field) => {
304
- const rel = this.relations.get(field.tableID);
305
- return {
306
- name: field.name,
307
- orgName: rel?.columns.get(field.columnID) ?? field.name,
308
- type: this.typeNames[field.dataTypeID] ?? `oid:${field.dataTypeID}`,
309
- table: rel?.table ?? "",
310
- orgTable: rel?.table ?? "",
311
- db: rel?.schema ?? "",
312
- maxLength: field.dataTypeSize > 0 ? field.dataTypeSize : 0
313
- };
314
- });
315
- return { fields, records: last.rows, affectedRows: 0, insertId: 0, isResultQuery: true };
316
- }
317
- return {
318
- fields: [],
319
- records: [],
320
- affectedRows: results.reduce((sum, r) => sum + (r.rowCount ?? 0), 0),
321
- insertId: 0,
322
- isResultQuery: false
323
- };
324
- }
325
- async setReadOnly() {
326
- await this.client.query("SET default_transaction_read_only = on");
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
- }
343
- async close() {
344
- await this.client.end();
345
- }
346
- };
347
- var postgresDriverFactory = {
348
- async open(config) {
349
- const client2 = new pg.Client(toClientConfig(config));
350
- await client2.connect();
351
- client2.on("error", (err) => console.error("[postgres] Connection error:", err.message));
352
- const driver = new PostgresDriver(client2, config);
353
- await driver.loadTypeNames();
354
- return driver;
355
- },
356
- async test(config) {
357
- const client2 = new pg.Client(toClientConfig(config));
358
- await client2.connect();
359
- await client2.end();
360
- },
361
- parseConfig(body) {
362
- const host = typeof body.host === "string" ? body.host.trim() : "";
363
- const user = typeof body.user === "string" ? body.user : "";
364
- if (!host || !user) {
365
- throw new Error("host and user are required");
366
- }
367
- return {
368
- driver: "postgres",
369
- host,
370
- port: Number(body.port) || 5432,
371
- user,
372
- password: typeof body.password === "string" ? body.password : "",
373
- database: typeof body.database === "string" && body.database ? body.database : "postgres",
374
- ssl: body.ssl === true,
375
- sslVerify: body.sslVerify === true
376
- };
377
- }
378
- };
379
-
380
- // src/drivers/sqlite.ts
381
- import { createRequire } from "module";
382
- import { existsSync, realpathSync } from "fs";
21
+ // src/install.ts
22
+ import { spawnSync } from "child_process";
23
+ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
383
24
  import { homedir } from "os";
384
- import { basename, dirname, join, relative, resolve, sep } from "path";
385
- function normalizeDeclaredType(declared) {
386
- if (!declared) return null;
387
- const base = declared.trim().toLowerCase().replace(/\(.*$/, "").trim();
388
- return base || null;
389
- }
390
- function inferTypeFromValue(value) {
391
- if (value === null || value === void 0) return "null";
392
- if (typeof value === "number") return Number.isInteger(value) ? "integer" : "real";
393
- if (typeof value === "bigint") return "bigint";
394
- if (Buffer.isBuffer(value)) return "blob";
395
- return "text";
396
- }
397
- var SQLITE_DIR = process.env.PLANCHE_SQLITE_DIR ? resolve(expandHome(process.env.PLANCHE_SQLITE_DIR)) : null;
398
- var DEFAULT_ROOT = homedir();
399
- var DENIED_UNDER_HOME = process.platform === "darwin" ? ["Library"] : process.platform === "win32" ? ["AppData"] : [];
400
- var ALLOW_ATTACH = process.env.PLANCHE_SQLITE_ATTACH === "1";
401
- var DB_EXTENSIONS = /\.(db|db3|s3db|sl3|sqlite|sqlite3)$/i;
402
- function expandHome(path) {
403
- return path === "~" || path.startsWith("~/") ? resolve(homedir(), path.slice(2)) : path;
404
- }
405
- function realish(file) {
406
- let probe = file;
407
- const rest = [];
408
- for (; ; ) {
409
- try {
410
- return rest.length ? join(realpathSync.native(probe), ...rest) : realpathSync.native(probe);
411
- } catch {
412
- const parent = dirname(probe);
413
- if (parent === probe) return file;
414
- rest.unshift(basename(probe));
415
- probe = parent;
416
- }
417
- }
418
- }
419
- function isInside(path, root) {
420
- return path === root || path.startsWith(root + sep);
421
- }
422
- function resolveFile(file, forCreate = false) {
423
- if (file === ":memory:") return file;
424
- if (file.startsWith("file:")) throw new Error("SQLite URI filenames are not supported by the agent; give a plain path");
425
- const resolved = realish(resolve(SQLITE_DIR ?? process.cwd(), expandHome(file)));
426
- if (SQLITE_DIR) {
427
- if (!isInside(resolved, realish(SQLITE_DIR))) throw new Error("SQLite file is outside the directory allowed by PLANCHE_SQLITE_DIR");
428
- } else {
429
- const root = realish(DEFAULT_ROOT);
430
- if (!isInside(resolved, root)) throw new Error("SQLite files outside your home directory need PLANCHE_SQLITE_DIR on the agent");
431
- const parts = relative(root, resolved).split(sep);
432
- if (parts.some((part) => part.startsWith("."))) throw new Error("SQLite files in hidden directories need PLANCHE_SQLITE_DIR on the agent");
433
- if (DENIED_UNDER_HOME.some((d) => d.toLowerCase() === parts[0].toLowerCase())) throw new Error(`SQLite files under ~/${parts[0]} need PLANCHE_SQLITE_DIR on the agent`);
434
- }
435
- if (forCreate && !DB_EXTENSIONS.test(resolved)) {
436
- throw new Error("A new SQLite file must end with .db, .db3, .s3db, .sl3, .sqlite or .sqlite3");
437
- }
438
- return resolved;
439
- }
440
- var ctor = null;
441
- function sqliteCtor() {
442
- ctor ??= createRequire(import.meta.url)("better-sqlite3");
443
- return ctor;
444
- }
445
- function openDatabase(config) {
446
- const file = resolveFile(config.file, config.createIfMissing === true);
447
- const isMemory = file === ":memory:";
448
- if (!isMemory && !config.createIfMissing && !existsSync(file)) {
449
- throw new Error(`SQLite file not found: ${config.file}`);
450
- }
451
- const db = new (sqliteCtor())(file, {
452
- readonly: config.readonly ?? false,
453
- fileMustExist: !isMemory && !config.createIfMissing
454
- });
455
- db.pragma("trusted_schema = OFF");
456
- if (config.readonly) db.pragma("query_only = ON");
457
- return db;
458
- }
459
- function stripLiteralsAndComments(sql2) {
460
- let out = "";
461
- let i = 0;
462
- while (i < sql2.length) {
463
- const c = sql2[i];
464
- if (c === "'" || c === '"' || c === "`") {
465
- let j = i + 1;
466
- while (j < sql2.length) {
467
- if (sql2[j] === c) {
468
- if (sql2[j + 1] === c) j += 2;
469
- else {
470
- j++;
471
- break;
472
- }
473
- } else j++;
474
- }
475
- out += c + c;
476
- i = j;
477
- continue;
478
- }
479
- if (c === "[") {
480
- const j = sql2.indexOf("]", i);
481
- i = j < 0 ? sql2.length : j + 1;
482
- out += "[]";
483
- continue;
484
- }
485
- if (c === "-" && sql2[i + 1] === "-") {
486
- const j = sql2.indexOf("\n", i);
487
- i = j < 0 ? sql2.length : j;
488
- out += " ";
489
- continue;
490
- }
491
- if (c === "/" && sql2[i + 1] === "*") {
492
- const j = sql2.indexOf("*/", i + 2);
493
- i = j < 0 ? sql2.length : j + 2;
494
- out += " ";
495
- continue;
496
- }
497
- out += c;
498
- i++;
499
- }
500
- return out;
501
- }
502
- function assertNoFileStatements(sql2) {
503
- const text = stripLiteralsAndComments(sql2);
504
- if (!ALLOW_ATTACH && /\bATTACH\b/i.test(text)) {
505
- throw new Error("ATTACH DATABASE is disabled on the agent (set PLANCHE_SQLITE_ATTACH=1 to allow it)");
506
- }
507
- if (/\bVACUUM\b[^;]*\bINTO\b/i.test(text)) {
508
- throw new Error("VACUUM INTO is not available through the agent");
509
- }
510
- if (/\bPRAGMA\s+temp_store_directory\b/i.test(text)) {
511
- throw new Error("PRAGMA temp_store_directory is not available through the agent");
512
- }
513
- }
514
- var SqliteDriver = class {
515
- constructor(db, config) {
516
- this.db = db;
517
- this.label = config.file;
518
- }
519
- db;
520
- type = "sqlite";
521
- label;
522
- async query(sql2) {
523
- assertNoFileStatements(sql2);
524
- let statement;
525
- try {
526
- statement = this.db.prepare(sql2);
527
- } catch (err) {
528
- if (err instanceof RangeError && /more than one statement/i.test(err.message)) {
529
- this.db.exec(sql2);
530
- return { fields: [], records: [], affectedRows: 0, insertId: 0, isResultQuery: false };
531
- }
532
- throw err;
533
- }
534
- if (!statement.reader) {
535
- const info = statement.run();
536
- return {
537
- fields: [],
538
- records: [],
539
- affectedRows: info.changes,
540
- insertId: Number(info.lastInsertRowid),
541
- isResultQuery: false
542
- };
543
- }
544
- const columns = statement.columns();
545
- const records = statement.raw(true).all();
546
- const fields = columns.map((col, index) => {
547
- const declared = normalizeDeclaredType(col.type);
548
- const type = declared ?? inferTypeFromValue(firstNonNull(records, index));
549
- return {
550
- name: col.name,
551
- orgName: col.column ?? col.name,
552
- type,
553
- table: col.table ?? "",
554
- orgTable: col.table ?? "",
555
- db: col.database ?? "",
556
- maxLength: 0
557
- };
558
- });
559
- return { fields, records, affectedRows: 0, insertId: 0, isResultQuery: true };
560
- }
561
- async setReadOnly() {
562
- this.db.pragma("query_only = 1");
563
- }
564
- async close() {
565
- if (this.db.inTransaction) {
566
- try {
567
- this.db.exec("ROLLBACK");
568
- } catch {
569
- }
570
- }
571
- this.db.close();
572
- }
573
- };
574
- function firstNonNull(records, index) {
575
- for (const row of records) {
576
- const value = row[index];
577
- if (value !== null && value !== void 0) return value;
578
- }
579
- return null;
580
- }
581
- var sqliteDriverFactory = {
582
- async open(config) {
583
- return new SqliteDriver(openDatabase(config), config);
584
- },
585
- async test(config) {
586
- const db = openDatabase(config);
587
- try {
588
- db.prepare("SELECT 1").get();
589
- } finally {
590
- db.close();
591
- }
592
- },
593
- parseConfig(body) {
594
- const file = typeof body.file === "string" ? body.file.trim() : "";
595
- if (!file) {
596
- throw new Error("file is required");
597
- }
598
- return {
599
- driver: "sqlite",
600
- file,
601
- createIfMissing: body.createIfMissing === true,
602
- readonly: body.readonly === true
603
- };
604
- }
605
- };
606
-
607
- // src/drivers/mssql.ts
608
- import sql from "mssql";
609
- function toPoolConfig(config) {
610
- return {
611
- server: config.host,
612
- port: config.port,
613
- user: config.user,
614
- password: config.password,
615
- database: config.database,
616
- // One physical connection: manual transactions and USE need session affinity.
617
- pool: { min: 1, max: 1, idleTimeoutMillis: 24 * 60 * 60 * 1e3 },
618
- options: {
619
- encrypt: config.ssl === true,
620
- trustServerCertificate: config.sslVerify !== true,
621
- // Keep date/time values as sent by the server rather than shifting to UTC.
622
- useUTC: false,
623
- enableArithAbort: true
624
- },
625
- requestTimeout: 0
626
- };
627
- }
628
- var pad = (n, width = 2) => String(n).padStart(width, "0");
629
- function formatDate(d) {
630
- const date = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
631
- const ms = d.getMilliseconds();
632
- return `${date} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}${ms ? "." + pad(ms, 3) : ""}`;
633
- }
634
- function normalizeValue(value) {
635
- if (value instanceof Date) return formatDate(value);
636
- if (Buffer.isBuffer(value)) return value.toString("hex");
637
- return value;
638
- }
639
- var RESULT_QUERY = /^\s*(?:WITH|SELECT|\()/i;
640
- var MssqlDriver = class {
641
- constructor(pool, config) {
642
- this.pool = pool;
643
- this.label = `${config.user}@${config.host}:${config.port}/${config.database}`;
644
- }
645
- pool;
646
- type = "mssql";
647
- label;
648
- /** Source columns per SQL text; `sys.dm_exec_describe_first_result_set` is not free. */
649
- sources = /* @__PURE__ */ new Map();
650
- /**
651
- * The `mssql` driver does not report which table a result column comes
652
- * from; ask the server to describe the statement instead so the grid can
653
- * decide whether it is editable and address rows by primary key.
654
- */
655
- async describe(text) {
656
- const cached4 = this.sources.get(text);
657
- if (cached4 !== void 0) return cached4;
658
- let result = null;
659
- try {
660
- const req = this.pool.request();
661
- req.input("sql", sql.NVarChar(sql.MAX), text);
662
- const res = await req.query(
663
- "SELECT name, source_schema, source_table, source_column, system_type_name FROM sys.dm_exec_describe_first_result_set(@sql, NULL, 1) WHERE is_hidden = 0 ORDER BY column_ordinal"
664
- );
665
- result = res.recordset.map((r) => ({
666
- schema: r.source_schema ?? "",
667
- table: r.source_table ?? "",
668
- column: r.source_column ?? "",
669
- type: (r.system_type_name ?? "").toLowerCase()
670
- }));
671
- } catch {
672
- result = null;
673
- }
674
- if (this.sources.size > 200) this.sources.clear();
675
- this.sources.set(text, result);
676
- return result;
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
- }
684
- async query(text) {
685
- const req = this.pool.request();
686
- req.arrayRowMode = true;
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
- }
694
- const sets = res.recordsets;
695
- const last = sets.length > 0 ? sets[sets.length - 1] : void 0;
696
- const columns = last?.columns;
697
- if (last && columns) {
698
- const meta = Object.values(columns).sort((a, b) => a.index - b.index);
699
- const sources = RESULT_QUERY.test(text) ? await this.describe(text) : null;
700
- const fields = meta.map((col, i) => {
701
- const src = sources && sources.length === meta.length ? sources[i] : void 0;
702
- const typeName = col.type?.declaration ?? "";
703
- return {
704
- name: col.name,
705
- orgName: src?.column || col.name,
706
- type: src?.type || typeName.toLowerCase(),
707
- table: src?.table ?? "",
708
- orgTable: src?.table ?? "",
709
- db: src?.schema ?? "",
710
- maxLength: typeof col.length === "number" ? col.length : 0
711
- };
712
- });
713
- const records = last.map((row) => row.map(normalizeValue));
714
- return { fields, records, affectedRows: 0, insertId: 0, isResultQuery: true };
715
- }
716
- return {
717
- fields: [],
718
- records: [],
719
- affectedRows: res.rowsAffected.reduce((sum, n) => sum + n, 0),
720
- insertId: 0,
721
- isResultQuery: false
722
- };
723
- }
724
- async close() {
725
- await this.pool.close();
726
- }
727
- };
728
- var mssqlDriverFactory = {
729
- async open(config) {
730
- const pool = new sql.ConnectionPool(toPoolConfig(config));
731
- await pool.connect();
732
- return new MssqlDriver(pool, config);
733
- },
734
- async test(config) {
735
- const pool = new sql.ConnectionPool(toPoolConfig(config));
736
- await pool.connect();
737
- await pool.close();
738
- },
739
- parseConfig(body) {
740
- const host = typeof body.host === "string" ? body.host.trim() : "";
741
- const user = typeof body.user === "string" ? body.user : "";
742
- if (!host || !user) {
743
- throw new Error("host and user are required");
744
- }
745
- return {
746
- driver: "mssql",
747
- host,
748
- port: Number(body.port) || 1433,
749
- user,
750
- password: typeof body.password === "string" ? body.password : "",
751
- database: typeof body.database === "string" && body.database ? body.database : "master",
752
- ssl: body.ssl === true,
753
- sslVerify: body.sslVerify === true
754
- };
755
- }
756
- };
757
-
758
- // src/drivers/oracle.ts
759
- import oracledb from "oracledb";
760
- oracledb.autoCommit = true;
761
- oracledb.fetchAsString = [oracledb.CLOB];
762
- var fetchTypeHandler = (meta) => meta.dbType === oracledb.DB_TYPE_NUMBER ? { type: oracledb.STRING } : void 0;
763
- var pad2 = (n, width = 2) => String(n).padStart(width, "0");
764
- function formatDate2(d) {
765
- const ms = d.getMilliseconds();
766
- return `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())} ${pad2(d.getHours())}:${pad2(d.getMinutes())}:${pad2(d.getSeconds())}${ms ? "." + pad2(ms, 3) : ""}`;
767
- }
768
- function normalizeValue2(value) {
769
- if (value instanceof Date) return formatDate2(value);
770
- if (Buffer.isBuffer(value)) return value.toString("hex");
771
- return value;
772
- }
773
- var SESSION_FORMATS = [
774
- "ALTER SESSION SET NLS_DATE_FORMAT = 'YYYY-MM-DD HH24:MI:SS'",
775
- "ALTER SESSION SET NLS_TIMESTAMP_FORMAT = 'YYYY-MM-DD HH24:MI:SS.FF3'",
776
- "ALTER SESSION SET NLS_TIMESTAMP_TZ_FORMAT = 'YYYY-MM-DD HH24:MI:SS.FF3 TZH:TZM'"
777
- ];
778
- var quoteIdent2 = (name) => `"${name.replaceAll('"', '""')}"`;
779
- var PLSQL = /^\s*(BEGIN|DECLARE|CREATE\s+(OR\s+REPLACE\s+)?((NON)?EDITIONABLE\s+)?(PROCEDURE|FUNCTION|TRIGGER|PACKAGE|TYPE)\b)/i;
780
- function normalizeStatement(sql2) {
781
- const trimmed = sql2.trim().replace(/\n\/\s*$/, "");
782
- if (PLSQL.test(trimmed)) return trimmed.endsWith(";") ? trimmed : `${trimmed};`;
783
- return trimmed.replace(/;\s*$/, "");
784
- }
785
- var SET_SCHEMA = /^\s*ALTER\s+SESSION\s+SET\s+CURRENT_SCHEMA\b/i;
786
- var START_TX = /^\s*SET\s+TRANSACTION\b/i;
787
- var END_TX = /^\s*(COMMIT|ROLLBACK)\b/i;
788
- function singleSourceTable(sql2) {
789
- const m = /\bFROM\s+("?[\w$#]+"?)(?:\s*\.\s*("?[\w$#]+"?))?([\s\S]*)$/i.exec(sql2);
790
- if (!m) return null;
791
- const rest = m[3].replace(/\b(WHERE|ORDER\s+BY|GROUP\s+BY|HAVING|FETCH|OFFSET|UNION|MINUS|INTERSECT|FOR\s+UPDATE)\b[\s\S]*$/i, "");
792
- if (/,|\bJOIN\b|\(/i.test(rest) || rest.trim().split(/\s+/).filter(Boolean).length > 2) return null;
793
- const unquote = (v) => v.startsWith('"') ? v.slice(1, -1).replaceAll('""', '"') : v.toUpperCase();
794
- return m[2] ? { schema: unquote(m[1]), table: unquote(m[2]) } : { table: unquote(m[1]) };
795
- }
796
- var OracleDriver = class {
797
- constructor(conn, config) {
798
- this.conn = conn;
799
- this.label = `${config.user}@${config.host}:${config.port}/${config.serviceName}`;
800
- this.currentSchema = config.user.toUpperCase();
801
- }
802
- conn;
803
- type = "oracle";
804
- label;
805
- currentSchema;
806
- manualTx = false;
807
- /** Column names per `OWNER.TABLE`, for result-source resolution. */
808
- columns = /* @__PURE__ */ new Map();
809
- async init() {
810
- for (const stmt of SESSION_FORMATS) await this.conn.execute(stmt);
811
- }
812
- async tableColumns(schema, table) {
813
- const key = `${schema}.${table}`;
814
- const cached4 = this.columns.get(key);
815
- if (cached4 !== void 0) return cached4;
816
- let out = null;
817
- try {
818
- const res = await this.conn.execute(
819
- "SELECT column_name FROM all_tab_columns WHERE owner = :o AND table_name = :t",
820
- { o: schema, t: table },
821
- { outFormat: oracledb.OUT_FORMAT_ARRAY }
822
- );
823
- out = res.rows && res.rows.length > 0 ? new Set(res.rows.map((r) => r[0])) : null;
824
- } catch {
825
- out = null;
826
- }
827
- if (this.columns.size > 200) this.columns.clear();
828
- this.columns.set(key, out);
829
- return out;
830
- }
831
- async query(sql2, database) {
832
- if (database && database !== this.currentSchema) {
833
- await this.conn.execute(`ALTER SESSION SET CURRENT_SCHEMA = ${quoteIdent2(database)}`);
834
- this.currentSchema = database;
835
- }
836
- const text = normalizeStatement(sql2);
837
- if (START_TX.test(text)) this.manualTx = true;
838
- const res = await this.conn.execute(text, [], {
839
- outFormat: oracledb.OUT_FORMAT_ARRAY,
840
- fetchTypeHandler,
841
- autoCommit: !this.manualTx
842
- });
843
- if (SET_SCHEMA.test(text)) this.currentSchema = null;
844
- if (END_TX.test(text)) this.manualTx = false;
845
- if (res.metaData && res.metaData.length > 0) {
846
- const source = singleSourceTable(text);
847
- const schema = source?.schema ?? this.currentSchema ?? "";
848
- const known = source && schema ? await this.tableColumns(schema, source.table) : null;
849
- const fields = res.metaData.map((col) => {
850
- const fromTable = known?.has(col.name) ?? false;
851
- return {
852
- name: col.name,
853
- orgName: col.name,
854
- type: (col.dbTypeName ?? "").toLowerCase(),
855
- table: fromTable ? source.table : "",
856
- orgTable: fromTable ? source.table : "",
857
- db: fromTable ? schema : "",
858
- maxLength: col.byteSize ?? 0
859
- };
860
- });
861
- const records = (res.rows ?? []).map((row) => row.map(normalizeValue2));
862
- return { fields, records, affectedRows: 0, insertId: 0, isResultQuery: true };
863
- }
864
- return { fields: [], records: [], affectedRows: res.rowsAffected ?? 0, insertId: 0, isResultQuery: false };
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
- }
874
- async close() {
875
- await this.conn.close();
876
- }
877
- };
878
- function connectionAttributes(config) {
879
- return {
880
- user: config.user,
881
- password: config.password,
882
- connectString: `${config.host}:${config.port}/${config.serviceName}`
883
- };
884
- }
885
- var oracleDriverFactory = {
886
- async open(config) {
887
- const conn = await oracledb.getConnection(connectionAttributes(config));
888
- const driver = new OracleDriver(conn, config);
889
- await driver.init();
890
- return driver;
891
- },
892
- async test(config) {
893
- const conn = await oracledb.getConnection(connectionAttributes(config));
894
- await conn.close();
895
- },
896
- parseConfig(body) {
897
- const host = typeof body.host === "string" ? body.host.trim() : "";
898
- const user = typeof body.user === "string" ? body.user : "";
899
- if (!host || !user) {
900
- throw new Error("host and user are required");
901
- }
902
- if (!/^[A-Za-z0-9._[\]-]{1,253}$/.test(host)) throw new Error("host is not a valid host name");
903
- const serviceName = typeof body.serviceName === "string" && body.serviceName ? body.serviceName.trim() : "FREEPDB1";
904
- if (!/^[A-Za-z0-9._$#-]{1,128}$/.test(serviceName)) throw new Error("serviceName may only contain letters, digits, . _ $ # -");
905
- return {
906
- driver: "oracle",
907
- host,
908
- port: Number(body.port) || 1521,
909
- user,
910
- password: typeof body.password === "string" ? body.password : "",
911
- serviceName
912
- };
913
- }
914
- };
915
-
916
- // src/drivers/d1.ts
917
- var API = "https://api.cloudflare.com/client/v4";
918
- var LOCAL_API = "/cdn-cgi/local/explorer/api";
919
- var SELECT_STAR = /^\s*SELECT\s+(?:DISTINCT\s+)?\*/i;
920
- var localRawUrl = (origin, databaseId) => `${origin}${LOCAL_API}/d1/database/${encodeURIComponent(databaseId)}/raw`;
921
- var LOOPBACK = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::1]", "::1", "0.0.0.0"]);
922
- function parseDevUrl(raw) {
923
- let url;
924
- try {
925
- url = new URL(raw);
926
- } catch {
927
- throw new Error(`Not a valid URL: ${raw}`);
928
- }
929
- if (url.protocol !== "http:" && url.protocol !== "https:") throw new Error("devUrl must be an http(s) URL");
930
- if (!LOOPBACK.has(url.hostname.toLowerCase())) {
931
- throw new Error("devUrl must point at a wrangler dev server on this machine (localhost).");
932
- }
933
- return url.origin;
934
- }
935
- var D1Error = class extends Error {
936
- constructor(message, status) {
937
- super(message);
938
- this.status = status;
939
- }
940
- status;
941
- };
942
- var SCHEMA_PRAGMA = /^\s*PRAGMA\s+(?:"main"|main|'main'|\[main\])\s*\.\s*/i;
943
- var DATABASE_LIST = /^\s*PRAGMA\s+database_list\s*;?\s*$/i;
944
- var RESULT_KIND = /^\s*(SELECT|WITH|PRAGMA|EXPLAIN|VALUES)\b/i;
945
- function singleSourceTable2(sql2) {
946
- const m = /\bFROM\s+(?:"?main"?\s*\.\s*)?("[^"]+"|`[^`]+`|\[[^\]]+\]|[\w$]+)([\s\S]*)$/i.exec(sql2);
947
- if (!m) return null;
948
- const rest = m[2].replace(/\b(WHERE|ORDER\s+BY|GROUP\s+BY|HAVING|LIMIT|OFFSET|UNION|EXCEPT|INTERSECT|WINDOW)\b[\s\S]*$/i, "");
949
- if (/,|\bJOIN\b|\(/i.test(rest) || rest.trim().split(/\s+/).filter(Boolean).length > 2) return null;
950
- const raw = m[1];
951
- const first = raw[0];
952
- if (first === '"') return raw.slice(1, -1).replaceAll('""', '"');
953
- if (first === "`") return raw.slice(1, -1).replaceAll("``", "`");
954
- if (first === "[") return raw.slice(1, -1);
955
- return raw;
956
- }
957
- function normalizeDeclaredType2(declared) {
958
- if (typeof declared !== "string") return null;
959
- const base = declared.trim().toLowerCase().replace(/\(.*$/, "").trim();
960
- return base || null;
961
- }
962
- function inferTypeFromValue2(value) {
963
- if (value === null || value === void 0) return "null";
964
- if (typeof value === "number") return Number.isInteger(value) ? "integer" : "real";
965
- if (Array.isArray(value)) return "blob";
966
- return "text";
967
- }
968
- function firstNonNull2(records, index) {
969
- for (const row of records) {
970
- const value = row[index];
971
- if (value !== null && value !== void 0) return value;
972
- }
973
- return null;
974
- }
975
- var D1Driver = class {
976
- type = "d1";
977
- label;
978
- /** Not readonly: a local database resolves its id lazily (see `resolveUrl`). */
979
- url;
980
- headers;
981
- /** Declared column types per table, for result-source resolution. */
982
- columns = /* @__PURE__ */ new Map();
983
- /**
984
- * D1 is a REST call, not a session: there is no cancel to send, so stopping
985
- * means dropping the request the agent is waiting on. The query may still
986
- * finish at Cloudflare, which is why this is `abandon` and not `kill`.
987
- */
988
- cancelKind = "abandon";
989
- inFlight = null;
990
- async cancel() {
991
- this.inFlight?.abort();
992
- }
993
- /** Local databases resolve their id on first use; the remote ones know it up front. */
994
- local;
995
- resolving = null;
996
- constructor(config) {
997
- this.local = config.devUrl ?? null;
998
- this.label = `d1:${config.databaseId || this.local || ""}`;
999
- if (this.local) {
1000
- this.headers = { "Content-Type": "application/json" };
1001
- this.url = config.databaseId ? localRawUrl(this.local, config.databaseId) : "";
1002
- } else {
1003
- this.headers = { Authorization: `Bearer ${config.apiToken ?? ""}`, "Content-Type": "application/json" };
1004
- this.url = `${API}/accounts/${encodeURIComponent(config.accountId ?? "")}/d1/database/${encodeURIComponent(config.databaseId ?? "")}/raw`;
1005
- }
1006
- }
1007
- /**
1008
- * A local dev server usually binds one database, so the connection form does
1009
- * not have to ask for a uuid it would only copy from wrangler's config.
1010
- */
1011
- async resolveUrl() {
1012
- if (this.url) return this.url;
1013
- this.resolving ??= (async () => {
1014
- const listed = await fetch(`${this.local}${LOCAL_API}/d1/database`, { signal: AbortSignal.timeout(1e4) });
1015
- if (!listed.ok) throw new D1Error(`No wrangler dev server answering at ${this.local}`, 400);
1016
- const body = await listed.json();
1017
- const bound = body.result ?? [];
1018
- if (bound.length === 0) throw new D1Error("That wrangler dev server has no D1 database bound.", 400);
1019
- const databases = [...new Map(bound.map((d) => [d.uuid ?? "", d])).values()];
1020
- if (databases.length > 1) {
1021
- const choices = databases.map((d) => `${d.name ?? "?"} (${d.uuid ?? "?"})`).join(", ");
1022
- throw new D1Error(
1023
- `That dev server binds ${databases.length} databases; put one of these ids in the Database ID field: ${choices}`,
1024
- 400
1025
- );
1026
- }
1027
- this.url = localRawUrl(this.local, databases[0].uuid ?? "");
1028
- return this.url;
1029
- })();
1030
- return this.resolving;
1031
- }
1032
- async raw(sql2) {
1033
- const url = this.url || await this.resolveUrl();
1034
- let res;
1035
- try {
1036
- const controller = new AbortController();
1037
- this.inFlight = controller;
1038
- try {
1039
- res = await fetch(url, {
1040
- method: "POST",
1041
- headers: this.headers,
1042
- body: JSON.stringify({ sql: sql2 }),
1043
- signal: AbortSignal.any([controller.signal, AbortSignal.timeout(6e4)])
1044
- });
1045
- } finally {
1046
- if (this.inFlight === controller) this.inFlight = null;
1047
- }
1048
- } catch (err) {
1049
- if (err instanceof Error && err.name === "AbortError") {
1050
- throw new D1Error("Stopped waiting for D1; the query may still finish at Cloudflare.", 400);
1051
- }
1052
- throw new D1Error(`Cannot reach the Cloudflare API: ${err instanceof Error ? err.message : String(err)}`, 400);
1053
- }
1054
- let body = null;
1055
- try {
1056
- body = await res.json();
1057
- } catch {
1058
- }
1059
- if (!res.ok || !body?.success) {
1060
- const message = (body?.errors?.map((e) => String(e.message)).join("; ") || `Cloudflare API responded ${res.status}`).slice(0, 500);
1061
- throw new D1Error(message, res.status === 401 || res.status === 403 ? 401 : res.ok ? void 0 : 400);
1062
- }
1063
- const result = body.result?.[0];
1064
- if (!result) throw new D1Error("Cloudflare API returned no result");
1065
- return result;
25
+ import { createInterface } from "readline/promises";
26
+ import { delimiter, dirname, join, resolve } from "path";
27
+ import { fileURLToPath } from "url";
28
+ var MANIFEST = "install.json";
29
+ var PLANCHE_DIR = join(homedir(), ".planche");
30
+ var UPDATE_CACHE = join(PLANCHE_DIR, "update-check.json");
31
+ var REGISTRY = "https://registry.npmjs.org/planche/latest";
32
+ var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
33
+ var cached = null;
34
+ function locate() {
35
+ if (cached) return cached;
36
+ const candidates = [];
37
+ if (process.env.PLANCHE_INSTALL_ROOT) candidates.push(resolve(process.env.PLANCHE_INSTALL_ROOT));
38
+ let dir = dirname(fileURLToPath(import.meta.url));
39
+ for (let i = 0; i < 6; i += 1) {
40
+ candidates.push(dir);
41
+ const parent = dirname(dir);
42
+ if (parent === dir) break;
43
+ dir = parent;
1066
44
  }
1067
- async tableColumns(table) {
1068
- const cached4 = this.columns.get(table);
1069
- if (cached4 !== void 0) return cached4;
1070
- let out = null;
45
+ for (const root of candidates) {
1071
46
  try {
1072
- const info = await this.raw(`PRAGMA table_info("${table.replaceAll('"', '""')}")`);
1073
- const cols = info.results?.columns ?? [];
1074
- const name = cols.indexOf("name");
1075
- const type = cols.indexOf("type");
1076
- const rows = info.results?.rows ?? [];
1077
- if (rows.length > 0 && name >= 0) {
1078
- out = new Map(rows.map((r) => [String(r[name]), normalizeDeclaredType2(type >= 0 ? r[type] : null)]));
47
+ const raw = readFileSync(join(root, MANIFEST), "utf8");
48
+ const manifest = JSON.parse(raw);
49
+ if (manifest && manifest.version === 1) {
50
+ cached = { root, manifest };
51
+ return cached;
1079
52
  }
1080
53
  } catch {
1081
- out = null;
1082
- }
1083
- if (this.columns.size > 200) this.columns.clear();
1084
- this.columns.set(table, out);
1085
- return out;
1086
- }
1087
- async query(sql2) {
1088
- if (DATABASE_LIST.test(sql2)) {
1089
- return {
1090
- fields: [
1091
- { name: "seq", orgName: "seq", type: "integer", table: "", orgTable: "", db: "", maxLength: 0 },
1092
- { name: "name", orgName: "name", type: "text", table: "", orgTable: "", db: "", maxLength: 0 },
1093
- { name: "file", orgName: "file", type: "text", table: "", orgTable: "", db: "", maxLength: 0 }
1094
- ],
1095
- records: [[0, "main", ""]],
1096
- affectedRows: 0,
1097
- insertId: 0,
1098
- isResultQuery: true
1099
- };
1100
- }
1101
- const text = sql2.replace(SCHEMA_PRAGMA, "PRAGMA ");
1102
- if (/^\s*(BEGIN|COMMIT|END|ROLLBACK|SAVEPOINT|RELEASE)\b/i.test(text)) {
1103
- throw new D1Error("Cloudflare D1 does not support transaction statements over the API; every request auto-commits.");
1104
- }
1105
- if (this.isDdl(text)) this.columns.clear();
1106
- const result = await this.raw(text);
1107
- const columns = result.results?.columns ?? [];
1108
- const records = result.results?.rows ?? [];
1109
- if (columns.length === 0 && !RESULT_KIND.test(text)) {
1110
- return {
1111
- fields: [],
1112
- records: [],
1113
- affectedRows: result.meta?.changes ?? 0,
1114
- insertId: result.meta?.last_row_id ?? 0,
1115
- isResultQuery: false
1116
- };
1117
- }
1118
- const source = RESULT_KIND.test(text) ? singleSourceTable2(text) : null;
1119
- const known = source ? await this.tableColumns(source) : null;
1120
- const names = columns.length > 0 ? columns : known && SELECT_STAR.test(text) ? [...known.keys()] : [];
1121
- const fields = names.map((name, index) => {
1122
- const fromTable = known?.has(name) ?? false;
1123
- const type = (fromTable ? known?.get(name) : null) ?? inferTypeFromValue2(firstNonNull2(records, index));
1124
- return {
1125
- name,
1126
- orgName: name,
1127
- type,
1128
- table: fromTable && source ? source : "",
1129
- orgTable: fromTable && source ? source : "",
1130
- db: fromTable ? "main" : "",
1131
- maxLength: 0
1132
- };
1133
- });
1134
- return { fields, records, affectedRows: 0, insertId: 0, isResultQuery: true };
1135
- }
1136
- isDdl(sql2) {
1137
- return /^\s*(CREATE|ALTER|DROP)\b/i.test(sql2);
1138
- }
1139
- async close() {
1140
- this.columns.clear();
1141
- }
1142
- };
1143
- var d1DriverFactory = {
1144
- async open(config) {
1145
- const driver = new D1Driver(config);
1146
- await driver.raw("SELECT 1");
1147
- return driver;
1148
- },
1149
- async test(config) {
1150
- await new D1Driver(config).raw("SELECT 1");
1151
- },
1152
- parseConfig(body) {
1153
- const accountId = typeof body.accountId === "string" ? body.accountId.trim() : "";
1154
- const databaseId = typeof body.databaseId === "string" ? body.databaseId.trim() : "";
1155
- const apiToken = typeof body.apiToken === "string" ? body.apiToken.trim() : "";
1156
- const devUrl = typeof body.devUrl === "string" ? body.devUrl.trim() : "";
1157
- if (devUrl) return { driver: "d1", devUrl: parseDevUrl(devUrl), databaseId: databaseId || void 0 };
1158
- if (!accountId || !databaseId || !apiToken) {
1159
- throw new Error("accountId, databaseId and apiToken are required (or devUrl for a local wrangler dev database)");
1160
54
  }
1161
- return { driver: "d1", accountId, databaseId, apiToken };
1162
55
  }
1163
- };
1164
-
1165
- // src/drivers/do.ts
1166
- var LOCAL_EXPLORER = "/cdn-cgi/local/explorer/api";
1167
- var CLOUDFLARE_API = "https://api.cloudflare.com/client/v4";
1168
- var MAX_OBJECTS = 5e3;
1169
- var DoError = class extends Error {
1170
- constructor(message, status) {
1171
- super(message);
1172
- this.status = status;
1173
- }
1174
- status;
1175
- };
1176
- var DATABASE_LIST2 = /^\s*PRAGMA\s+database_list\s*;?\s*$/i;
1177
- var RESULT_KIND2 = /^\s*(SELECT|WITH|PRAGMA|EXPLAIN|VALUES)\b/i;
1178
- var TRANSACTION = /^\s*(BEGIN|COMMIT|END|ROLLBACK|SAVEPOINT|RELEASE)\b/i;
1179
- var DDL = /^\s*(CREATE|ALTER|DROP)\b/i;
1180
- var HEX_ID = /^[0-9a-f]{64}$/i;
1181
- function singleSourceTable3(sql2) {
1182
- const m = /\bFROM\s+("[^"]+"|`[^`]+`|\[[^\]]+\]|[\w$]+)([\s\S]*)$/i.exec(sql2);
1183
- if (!m) return null;
1184
- const rest = m[2].replace(
1185
- /\b(WHERE|ORDER\s+BY|GROUP\s+BY|HAVING|LIMIT|OFFSET|UNION|EXCEPT|INTERSECT|WINDOW)\b[\s\S]*$/i,
1186
- ""
1187
- );
1188
- if (/,|\bJOIN\b|\(/i.test(rest) || rest.trim().split(/\s+/).filter(Boolean).length > 2) return null;
1189
- const raw = m[1];
1190
- const first = raw[0];
1191
- if (first === '"') return raw.slice(1, -1).replaceAll('""', '"');
1192
- if (first === "`") return raw.slice(1, -1).replaceAll("``", "`");
1193
- if (first === "[") return raw.slice(1, -1);
1194
- return raw;
1195
- }
1196
- function normalizeDeclaredType3(declared) {
1197
- if (typeof declared !== "string") return null;
1198
- const base = declared.trim().toLowerCase().replace(/\(.*$/, "").trim();
1199
- return base || null;
1200
- }
1201
- function inferTypeFromValue3(value) {
1202
- if (value === null || value === void 0) return "null";
1203
- if (typeof value === "number") return Number.isInteger(value) ? "integer" : "real";
1204
- if (Array.isArray(value)) return "blob";
1205
- return "text";
1206
- }
1207
- function firstNonNull3(records, index) {
1208
- for (const row of records) {
1209
- const value = row[index];
1210
- if (value !== null && value !== void 0) return value;
1211
- }
1212
- return null;
1213
- }
1214
- function closingQuote(sql2, start, close) {
1215
- let i = start + 1;
1216
- while (i < sql2.length) {
1217
- if (sql2[i] === close) {
1218
- if (sql2[i + 1] === close && close !== "]") {
1219
- i += 2;
1220
- continue;
1221
- }
1222
- return i + 1;
1223
- }
1224
- i += 1;
1225
- }
1226
- return sql2.length;
1227
- }
1228
- function skipSpace(sql2, from) {
1229
- let i = from;
1230
- while (i < sql2.length && /\s/.test(sql2[i])) i += 1;
1231
- return i;
1232
- }
1233
- function routeStatement(sql2, known) {
1234
- let out = "";
1235
- let target = null;
1236
- let i = 0;
1237
- while (i < sql2.length) {
1238
- const ch = sql2[i];
1239
- if (ch === "'") {
1240
- const end = closingQuote(sql2, i, "'");
1241
- out += sql2.slice(i, end);
1242
- i = end;
1243
- continue;
1244
- }
1245
- if (ch === "-" && sql2[i + 1] === "-") {
1246
- const end = sql2.indexOf("\n", i);
1247
- const stop = end === -1 ? sql2.length : end;
1248
- out += sql2.slice(i, stop);
1249
- i = stop;
1250
- continue;
1251
- }
1252
- if (ch === "/" && sql2[i + 1] === "*") {
1253
- const end = sql2.indexOf("*/", i + 2);
1254
- const stop = end === -1 ? sql2.length : end + 2;
1255
- out += sql2.slice(i, stop);
1256
- i = stop;
1257
- continue;
1258
- }
1259
- if (ch === '"' || ch === "`" || ch === "[") {
1260
- const close = ch === "[" ? "]" : ch;
1261
- const end = closingQuote(sql2, i, close);
1262
- const raw = sql2.slice(i, end);
1263
- const inner = ch === "[" ? raw.slice(1, -1) : raw.slice(1, -1).replaceAll(close + close, close);
1264
- const dot = skipSpace(sql2, end);
1265
- if (sql2[dot] === "." && known(inner)) {
1266
- target ??= inner;
1267
- i = dot + 1;
1268
- continue;
1269
- }
1270
- out += raw;
1271
- i = end;
1272
- continue;
1273
- }
1274
- out += ch;
1275
- i += 1;
1276
- }
1277
- return { sql: out, target };
1278
- }
1279
- async function fetchJson(url, init, unreachable, abort) {
1280
- let res;
1281
- try {
1282
- const signal = abort ? AbortSignal.any([abort, AbortSignal.timeout(6e4)]) : AbortSignal.timeout(6e4);
1283
- res = await fetch(url, { ...init, signal });
1284
- } catch (err) {
1285
- if (err instanceof Error && err.name === "AbortError" && abort?.aborted) {
1286
- throw new DoError("Stopped waiting for the request; the query may still finish.", 400);
1287
- }
1288
- throw unreachable(err instanceof Error ? err.message : String(err));
1289
- }
1290
- let body = null;
1291
- try {
1292
- body = await res.json();
1293
- } catch {
1294
- }
1295
- return { res, body };
1296
- }
1297
- var LocalTransport = class {
1298
- label;
1299
- base;
1300
- /** The request being waited on, so a stop can drop it. */
1301
- inFlight = null;
1302
- abort() {
1303
- this.inFlight?.abort();
1304
- }
1305
- /** Signal for the next request; replaces whatever was tracked before. */
1306
- track() {
1307
- this.inFlight = new AbortController();
1308
- return this.inFlight.signal;
1309
- }
1310
- constructor(devUrl) {
1311
- this.label = `do:${devUrl}`;
1312
- this.base = `${devUrl.replace(/\/+$/, "")}${LOCAL_EXPLORER}`;
1313
- }
1314
- async call(path, body) {
1315
- const origin = this.base.replace(LOCAL_EXPLORER, "");
1316
- const { res, body: parsed } = await fetchJson(
1317
- `${this.base}${path}`,
1318
- {
1319
- method: body === void 0 ? "GET" : "POST",
1320
- headers: body === void 0 ? void 0 : { "Content-Type": "application/json" },
1321
- body: body === void 0 ? void 0 : JSON.stringify(body)
1322
- },
1323
- (reason) => new DoError(
1324
- `Cannot reach a wrangler dev server at ${origin} (${reason}). Run \`wrangler dev\` in your Worker project and try again.`,
1325
- 400
1326
- ),
1327
- this.track()
1328
- );
1329
- const envelope = parsed && typeof parsed.success === "boolean" && (parsed.result !== void 0 || Array.isArray(parsed.errors)) ? parsed : null;
1330
- if (!envelope) {
1331
- throw new DoError(
1332
- `${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.`,
1333
- 400
1334
- );
1335
- }
1336
- if (!res.ok || !envelope.success) {
1337
- throw new DoError(errorText(envelope.errors) || `wrangler dev responded ${res.status}`, res.ok ? void 0 : 400);
1338
- }
1339
- if (envelope.result === void 0) throw new DoError("wrangler dev returned no result");
1340
- return envelope.result;
1341
- }
1342
- async namespaces() {
1343
- const list = await this.call("/workers/durable_objects/namespaces");
1344
- return list.map((ns) => ({ id: ns.id, sqlite: ns.use_sqlite !== false }));
1345
- }
1346
- objects(namespaceId) {
1347
- return this.call(`/workers/durable_objects/namespaces/${encodeURIComponent(namespaceId)}/objects`);
1348
- }
1349
- async run(target, sql2) {
1350
- const selector = target.name ? { durable_object_name: target.name } : { durable_object_id: target.id };
1351
- const results = await this.call(
1352
- `/workers/durable_objects/namespaces/${encodeURIComponent(target.namespaceId)}/query`,
1353
- { ...selector, queries: [{ sql: sql2 }] }
1354
- );
1355
- const result = results[0];
1356
- if (!result) throw new DoError("wrangler dev returned no result");
1357
- return result;
1358
- }
1359
- };
1360
- var RemoteTransport = class {
1361
- label;
1362
- base;
1363
- /** The request being waited on, so a stop can drop it. */
1364
- inFlight = null;
1365
- abort() {
1366
- this.inFlight?.abort();
1367
- }
1368
- /** Signal for the next request; replaces whatever was tracked before. */
1369
- track() {
1370
- this.inFlight = new AbortController();
1371
- return this.inFlight.signal;
1372
- }
1373
- headers;
1374
- only;
1375
- constructor(config) {
1376
- this.label = `do:${config.accountId}${config.namespaceId ? `/${config.namespaceId}` : ""}`;
1377
- this.base = `${CLOUDFLARE_API}/accounts/${encodeURIComponent(config.accountId)}/workers/durable_objects/namespaces`;
1378
- this.headers = { Authorization: `Bearer ${config.apiToken}`, "Content-Type": "application/json" };
1379
- this.only = config.namespaceId || void 0;
1380
- }
1381
- async call(path, body) {
1382
- const { res, body: parsed } = await fetchJson(
1383
- `${this.base}${path}`,
1384
- {
1385
- method: body === void 0 ? "GET" : "POST",
1386
- headers: this.headers,
1387
- body: body === void 0 ? void 0 : JSON.stringify(body)
1388
- },
1389
- (reason) => new DoError(`Cannot reach the Cloudflare API: ${reason}`, 400),
1390
- this.track()
1391
- );
1392
- if (res.status === 401 || res.status === 403) {
1393
- throw new DoError(
1394
- errorText(parsed?.errors) || "Cloudflare refused the API token. It needs the Workers Scripts Read permission on this account.",
1395
- 401
1396
- );
1397
- }
1398
- if (res.status === 404 || res.status === 405) {
1399
- throw new DoError(
1400
- "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.",
1401
- 400
1402
- );
1403
- }
1404
- if (!res.ok || !parsed?.success) {
1405
- throw new DoError(errorText(parsed?.errors) || `Cloudflare API responded ${res.status}`, 400);
1406
- }
1407
- return parsed;
1408
- }
1409
- async namespaces() {
1410
- if (this.only) return [{ id: this.only, sqlite: true }];
1411
- const body = await this.call("");
1412
- return (body.result ?? []).map((ns) => ({ id: ns.id, sqlite: ns.use_sqlite !== false }));
1413
- }
1414
- /**
1415
- * Cloudflare's listing returns ids and `hasStoredData`, never names. It also
1416
- * answers an empty page for small `limit` values, so the parameter is left
1417
- * off and the default page size is used, following `result_info.cursor`.
1418
- */
1419
- async objects(namespaceId) {
1420
- const out = [];
1421
- let cursor;
1422
- do {
1423
- const query = cursor ? `?cursor=${encodeURIComponent(cursor)}` : "";
1424
- const body = await this.call(
1425
- `/${encodeURIComponent(namespaceId)}/objects${query}`
1426
- );
1427
- for (const object of body.result ?? []) out.push({ id: object.id });
1428
- cursor = body.result_info?.cursor || void 0;
1429
- } while (cursor && out.length < MAX_OBJECTS);
1430
- return out;
1431
- }
1432
- async run(target, sql2) {
1433
- if (!target.id) {
1434
- throw new DoError(
1435
- `"${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.`,
1436
- 400
1437
- );
1438
- }
1439
- const body = await this.call(
1440
- `/${encodeURIComponent(target.namespaceId)}/query`,
1441
- { durable_object_id: target.id, queries: [{ sql: sql2 }] }
1442
- );
1443
- const result = body.result?.results?.[0];
1444
- if (!result) throw new DoError("Cloudflare returned no result");
1445
- return result;
1446
- }
1447
- };
1448
- function errorText(errors) {
1449
- if (!Array.isArray(errors)) return "";
1450
- return errors.map((e) => typeof e === "string" ? e : String(e?.message ?? "")).filter(Boolean).join("; ").slice(0, 500);
1451
- }
1452
- var DoDriver = class {
1453
- constructor(type, transport) {
1454
- this.type = type;
1455
- this.transport = transport;
1456
- this.label = transport.label;
1457
- }
1458
- type;
1459
- transport;
1460
- label;
1461
- /** Database name -> the object it addresses, refreshed by every listing. */
1462
- targets = /* @__PURE__ */ new Map();
1463
- /** Declared column types per `database table`, for result-source resolution. */
1464
- columns = /* @__PURE__ */ new Map();
1465
- /**
1466
- * These are HTTP calls, not sessions: there is nothing to cancel upstream, so
1467
- * stopping drops the request the agent waits on. `abandon`, not `kill`.
1468
- */
1469
- cancelKind = "abandon";
1470
- async cancel() {
1471
- this.transport.abort();
1472
- }
1473
- /**
1474
- * Every object of every SQLite-backed namespace, as app-level database
1475
- * names. Locally, a `wrangler dev` session registers itself in wrangler's
1476
- * dev registry, so sessions of other projects show up here too.
1477
- */
1478
- async listDatabases() {
1479
- const namespaces = await this.transport.namespaces();
1480
- const names = [];
1481
- this.targets.clear();
1482
- for (const ns of namespaces) {
1483
- if (!ns.sqlite) continue;
1484
- let objects;
1485
- try {
1486
- objects = await this.transport.objects(ns.id);
1487
- } catch {
1488
- continue;
1489
- }
1490
- for (const object of objects) {
1491
- const ref = object.name || object.id;
1492
- if (!ref) continue;
1493
- const database = `${ns.id}/${ref}`;
1494
- this.targets.set(database, { namespaceId: ns.id, name: object.name, id: object.id });
1495
- names.push(database);
1496
- }
1497
- }
1498
- return names.sort();
1499
- }
1500
- /**
1501
- * Resolve a database name to the object it addresses. Names are cached by
1502
- * the last listing; one that was never listed is still usable, so a saved
1503
- * query tab keeps working — the trailing part is read as an id when it looks
1504
- * like one and as a name otherwise.
1505
- */
1506
- targetFor(database) {
1507
- const cached4 = this.targets.get(database);
1508
- if (cached4) return cached4;
1509
- const slash = database.indexOf("/");
1510
- if (slash <= 0 || slash === database.length - 1) {
1511
- throw new DoError(
1512
- `"${database}" is not a Durable Object. Pick one from the schema tree (they are named "<namespace>/<object>").`,
1513
- 400
1514
- );
1515
- }
1516
- const namespaceId = database.slice(0, slash);
1517
- const ref = database.slice(slash + 1);
1518
- return HEX_ID.test(ref) ? { namespaceId, id: ref } : { namespaceId, name: ref };
1519
- }
1520
- async tableColumns(database, target, table) {
1521
- const key = `${database} ${table}`;
1522
- const cached4 = this.columns.get(key);
1523
- if (cached4 !== void 0) return cached4;
1524
- let out = null;
1525
- try {
1526
- const info = await this.transport.run(target, `PRAGMA table_info("${table.replaceAll('"', '""')}")`);
1527
- const cols = info.columns ?? [];
1528
- const name = cols.indexOf("name");
1529
- const type = cols.indexOf("type");
1530
- const rows = info.rows ?? [];
1531
- if (rows.length > 0 && name >= 0) {
1532
- out = new Map(rows.map((r) => [String(r[name]), normalizeDeclaredType3(type >= 0 ? r[type] : null)]));
1533
- }
1534
- } catch {
1535
- out = null;
1536
- }
1537
- if (this.columns.size > 200) this.columns.clear();
1538
- this.columns.set(key, out);
1539
- return out;
1540
- }
1541
- async query(sql2, database) {
1542
- if (DATABASE_LIST2.test(sql2)) {
1543
- const names = await this.listDatabases();
1544
- return {
1545
- fields: ["seq", "name", "file"].map((name2) => ({
1546
- name: name2,
1547
- orgName: name2,
1548
- type: name2 === "seq" ? "integer" : "text",
1549
- table: "",
1550
- orgTable: "",
1551
- db: "",
1552
- maxLength: 0
1553
- })),
1554
- records: names.map((name2, seq) => [seq, name2, ""]),
1555
- affectedRows: 0,
1556
- insertId: 0,
1557
- isResultQuery: true
1558
- };
1559
- }
1560
- const routed = routeStatement(sql2, (name2) => this.targets.has(name2) || name2.includes("/"));
1561
- const text = routed.sql;
1562
- if (TRANSACTION.test(text)) {
1563
- throw new DoError(
1564
- "Durable Object storage does not take transaction statements over this API; every request auto-commits."
1565
- );
1566
- }
1567
- const name = routed.target ?? database;
1568
- if (!name) {
1569
- throw new DoError(
1570
- 'No Durable Object selected. Pick one in the schema tree, or qualify the table as "<namespace>/<object>"."<table>".',
1571
- 400
1572
- );
1573
- }
1574
- const target = this.targetFor(name);
1575
- if (DDL.test(text)) this.columns.clear();
1576
- const result = await this.transport.run(target, text);
1577
- const columns = result.columns ?? [];
1578
- const records = result.rows ?? [];
1579
- if (columns.length === 0 && !RESULT_KIND2.test(text)) {
1580
- return {
1581
- fields: [],
1582
- records: [],
1583
- affectedRows: result.meta?.rows_written ?? 0,
1584
- insertId: 0,
1585
- isResultQuery: false
1586
- };
1587
- }
1588
- const source = RESULT_KIND2.test(text) ? singleSourceTable3(text) : null;
1589
- const known = source ? await this.tableColumns(name, target, source) : null;
1590
- const fields = columns.map((column, index) => {
1591
- const fromTable = known?.has(column) ?? false;
1592
- const type = (fromTable ? known?.get(column) : null) ?? inferTypeFromValue3(firstNonNull3(records, index));
1593
- return {
1594
- name: column,
1595
- orgName: column,
1596
- type,
1597
- table: fromTable && source ? source : "",
1598
- orgTable: fromTable && source ? source : "",
1599
- db: fromTable ? name : "",
1600
- maxLength: 0
1601
- };
1602
- });
1603
- return { fields, records, affectedRows: 0, insertId: 0, isResultQuery: true };
1604
- }
1605
- async close() {
1606
- this.targets.clear();
1607
- this.columns.clear();
1608
- }
1609
- };
1610
- var LOOPBACK2 = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::1]", "::1", "0.0.0.0"]);
1611
- function parseDevUrl2(raw) {
1612
- let url;
1613
- try {
1614
- url = new URL(raw);
1615
- } catch {
1616
- throw new Error(`Not a valid URL: ${raw}`);
1617
- }
1618
- if (url.protocol !== "http:" && url.protocol !== "https:") {
1619
- throw new Error("devUrl must be an http(s) URL");
1620
- }
1621
- if (!LOOPBACK2.has(url.hostname.toLowerCase())) {
1622
- throw new Error(
1623
- "devUrl must point at a wrangler dev server on this machine (localhost). Use the deployed Durable Objects connection for objects that are not local."
1624
- );
1625
- }
1626
- return url.origin;
1627
- }
1628
- var doDriverFactory = {
1629
- async open(config) {
1630
- const driver = new DoDriver("do", new LocalTransport(config.devUrl));
1631
- await driver.query("PRAGMA database_list");
1632
- return driver;
1633
- },
1634
- async test(config) {
1635
- await new DoDriver("do", new LocalTransport(config.devUrl)).query("PRAGMA database_list");
1636
- },
1637
- parseConfig(body) {
1638
- const devUrl = typeof body.devUrl === "string" ? body.devUrl.trim() : "";
1639
- if (!devUrl) throw new Error("devUrl is required");
1640
- return { driver: "do", devUrl: parseDevUrl2(devUrl) };
1641
- }
1642
- };
1643
- var doRemoteDriverFactory = {
1644
- async open(config) {
1645
- const driver = new DoDriver("do-remote", new RemoteTransport(config));
1646
- await driver.query("PRAGMA database_list");
1647
- return driver;
1648
- },
1649
- async test(config) {
1650
- await new DoDriver("do-remote", new RemoteTransport(config)).query("PRAGMA database_list");
1651
- },
1652
- parseConfig(body) {
1653
- const accountId = typeof body.accountId === "string" ? body.accountId.trim() : "";
1654
- const apiToken = typeof body.apiToken === "string" ? body.apiToken.trim() : "";
1655
- const namespaceId = typeof body.namespaceId === "string" ? body.namespaceId.trim() : "";
1656
- if (!accountId || !apiToken) throw new Error("accountId and apiToken are required");
1657
- return { driver: "do-remote", accountId, apiToken, ...namespaceId ? { namespaceId } : {} };
1658
- }
1659
- };
1660
-
1661
- // src/ssh-tunnel.ts
1662
- import { createServer } from "net";
1663
- import { mkdir, readFile as readFile2, appendFile } from "fs/promises";
1664
- import { createHash, createHmac, timingSafeEqual } from "crypto";
1665
- import { homedir as homedir3 } from "os";
1666
- import { basename as basename3, join as join3 } from "path";
1667
- import { Client } from "ssh2";
1668
-
1669
- // src/ssh-config.ts
1670
- import { readFile, readdir } from "fs/promises";
1671
- import { homedir as homedir2 } from "os";
1672
- import { dirname as dirname2, isAbsolute, join as join2, basename as basename2 } from "path";
1673
- var SSH_CONFIG_DISABLED = process.env.PLANCHE_SSH_CONFIG === "none";
1674
- var DEFAULT_SSH_CONFIG = SSH_CONFIG_DISABLED ? "" : process.env.PLANCHE_SSH_CONFIG || join2(homedir2(), ".ssh", "config");
1675
- var DEFAULT_IDENTITY_FILES = ["id_ed25519", "id_ecdsa", "id_rsa", "id_ed25519_sk", "id_ecdsa_sk"].map(
1676
- (f) => join2(homedir2(), ".ssh", f)
1677
- );
1678
- function expandHome2(path) {
1679
- return path === "~" || path.startsWith("~/") ? join2(homedir2(), path.slice(1)) : path;
1680
- }
1681
- function globToRegExp(pattern) {
1682
- const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".");
1683
- return new RegExp(`^${escaped}$`, "i");
1684
- }
1685
- function hostPatternMatches(patterns, host) {
1686
- let matched = false;
1687
- for (const raw of patterns) {
1688
- const negated = raw.startsWith("!");
1689
- const pattern = negated ? raw.slice(1) : raw;
1690
- if (!globToRegExp(pattern).test(host)) continue;
1691
- if (negated) return false;
1692
- matched = true;
1693
- }
1694
- return matched;
1695
- }
1696
- function tokenize(line) {
1697
- const tokens = [];
1698
- const re = /"([^"]*)"|(\S+)/g;
1699
- let m;
1700
- while (m = re.exec(line)) tokens.push(m[1] ?? m[2]);
1701
- return tokens;
1702
- }
1703
- function parseLine(line) {
1704
- const trimmed = line.trim();
1705
- if (!trimmed || trimmed.startsWith("#")) return void 0;
1706
- const eq = trimmed.match(/^([A-Za-z]+)\s*=\s*(.*)$/);
1707
- const [keyword, rest] = eq ? [eq[1], eq[2]] : [trimmed.split(/\s+/, 1)[0], trimmed.slice(trimmed.split(/\s+/, 1)[0].length)];
1708
- return { keyword: keyword.toLowerCase(), args: tokenize(rest) };
1709
- }
1710
- async function expandGlob(path) {
1711
- const base = basename2(path);
1712
- if (!/[*?]/.test(base)) return [path];
1713
- const dir = dirname2(path);
1714
- try {
1715
- const re = globToRegExp(base);
1716
- return (await readdir(dir)).filter((name) => re.test(name)).sort().map((name) => join2(dir, name));
1717
- } catch {
1718
- return [];
1719
- }
1720
- }
1721
- async function loadLines(file, seen) {
1722
- if (seen.has(file)) return [];
1723
- seen.add(file);
1724
- try {
1725
- return (await readFile(file, "utf8")).split(/\r?\n/);
1726
- } catch {
1727
- return [];
1728
- }
1729
- }
1730
- async function resolveSshHost(host, file = DEFAULT_SSH_CONFIG) {
1731
- const result = { identityFiles: [] };
1732
- if (!file) return result;
1733
- const seen = /* @__PURE__ */ new Set();
1734
- async function walk(path, active) {
1735
- let blockActive = active;
1736
- for (const line of await loadLines(path, seen)) {
1737
- const d = parseLine(line);
1738
- if (!d) continue;
1739
- switch (d.keyword) {
1740
- case "host":
1741
- blockActive = hostPatternMatches(d.args, host);
1742
- continue;
1743
- case "match":
1744
- blockActive = d.args.length === 1 && d.args[0].toLowerCase() === "all";
1745
- continue;
1746
- case "include":
1747
- for (const inc of d.args) {
1748
- const target = expandHome2(inc);
1749
- const abs = isAbsolute(target) ? target : join2(homedir2(), ".ssh", target);
1750
- for (const expanded of await expandGlob(abs)) await walk(expanded, blockActive);
1751
- }
1752
- continue;
1753
- }
1754
- if (!blockActive) continue;
1755
- const value = d.args[0];
1756
- if (value === void 0) continue;
1757
- switch (d.keyword) {
1758
- case "hostname":
1759
- result.hostName ??= value.replace(/%h/g, host);
1760
- break;
1761
- case "port":
1762
- result.port ??= Number(value) || void 0;
1763
- break;
1764
- case "user":
1765
- result.user ??= value;
1766
- break;
1767
- case "identityfile":
1768
- for (const f of d.args) {
1769
- const expanded = expandHome2(f.replace(/%d/g, homedir2()).replace(/%h/g, host));
1770
- if (expanded.split(/[\\/]/).includes("..")) continue;
1771
- result.identityFiles.push(expanded);
1772
- }
1773
- break;
1774
- case "identityagent":
1775
- result.identityAgent ??= value === "none" ? void 0 : expandHome2(value.replace(/%d/g, homedir2()));
1776
- break;
1777
- case "proxyjump":
1778
- result.proxyJump ??= value;
1779
- break;
1780
- }
1781
- }
1782
- }
1783
- await walk(file, true);
1784
- return result;
1785
- }
1786
-
1787
- // src/ssh-tunnel.ts
1788
- var TunnelError = class extends Error {
1789
- constructor(message, status) {
1790
- super(message);
1791
- this.status = status;
1792
- this.name = "TunnelError";
1793
- }
1794
- status;
1795
- };
1796
- var HostKeyError = class extends TunnelError {
1797
- hostKey;
1798
- constructor(host, port, type, fingerprint2) {
1799
- super(`The SSH host key of ${host}:${port} is not known yet (${type} ${fingerprint2}). Verify the fingerprint and accept it to continue.`, 409);
1800
- this.name = "HostKeyError";
1801
- this.hostKey = { host, port, type, fingerprint: fingerprint2 };
1802
- }
1803
- };
1804
- var CONNECT_TIMEOUT_MS = 2e4;
1805
- var PLANCHE_DIR = join3(homedir3(), ".planche");
1806
- var PLANCHE_KNOWN_HOSTS = join3(PLANCHE_DIR, "known_hosts");
1807
- var OPENSSH_KNOWN_HOSTS = join3(homedir3(), ".ssh", "known_hosts");
1808
- function parseSshConfig(value) {
1809
- if (value === void 0 || value === null) return void 0;
1810
- if (typeof value !== "object") throw new TunnelError("ssh must be an object", 400);
1811
- const body = value;
1812
- const host = typeof body.host === "string" ? body.host.trim() : "";
1813
- const user = typeof body.user === "string" ? body.user.trim() : "";
1814
- const auth = body.auth;
1815
- if (!host) throw new TunnelError("ssh.host is required", 400);
1816
- if (host.length > 253 || !/^[A-Za-z0-9._:[\]-]+$/.test(host) || /^[.-]+$/.test(host)) throw new TunnelError("ssh.host is not a valid host name", 400);
1817
- if (user.length > 64 || /[\s\x00-\x1f]/.test(user)) throw new TunnelError("ssh.user is not valid", 400);
1818
- const port = Math.trunc(Number(body.port)) || 22;
1819
- if (port < 1 || port > 65535) throw new TunnelError("ssh.port must be between 1 and 65535", 400);
1820
- if (auth !== "password" && auth !== "key" && auth !== "agent") {
1821
- throw new TunnelError("ssh.auth must be one of password, key, agent", 400);
1822
- }
1823
- const str = (v) => typeof v === "string" && v ? v : void 0;
1824
- const config = {
1825
- host,
1826
- port,
1827
- user,
1828
- auth,
1829
- password: auth === "password" ? str(body.password) ?? "" : void 0,
1830
- privateKey: auth === "key" ? str(body.privateKey) : void 0,
1831
- passphrase: auth === "key" ? str(body.passphrase) : void 0,
1832
- acceptHostKey: typeof body.acceptHostKey === "string" && /^SHA256:[A-Za-z0-9+/]{43}$/.test(body.acceptHostKey) ? body.acceptHostKey : void 0
1833
- };
1834
- return config;
1835
- }
1836
- function keyType(blob) {
1837
- if (blob.length < 4) throw new TunnelError("Malformed host key", 400);
1838
- const len = blob.readUInt32BE(0);
1839
- if (len <= 0 || len > 64 || blob.length < 4 + len) throw new TunnelError("Malformed host key", 400);
1840
- const type = blob.subarray(4, 4 + len).toString("ascii");
1841
- if (!/^[A-Za-z0-9@.-]+$/.test(type)) throw new TunnelError("Malformed host key", 400);
1842
- return type;
1843
- }
1844
- function knownHostsEntryMatches(field, patterns) {
1845
- if (field.startsWith("|1|")) {
1846
- const [, , salt, hash] = field.split("|");
1847
- if (!salt || !hash) return false;
1848
- const key = Buffer.from(salt, "base64");
1849
- const want = Buffer.from(hash, "base64");
1850
- return patterns.some((p) => {
1851
- const got = createHmac("sha1", key).update(p).digest();
1852
- return got.length === want.length && timingSafeEqual(got, want);
1853
- });
1854
- }
1855
- const entries = field.toLowerCase().split(",");
1856
- return patterns.some((p) => hostPatternMatches(entries, p));
1857
- }
1858
- function hostPatterns(host, port) {
1859
- const h = host.toLowerCase();
1860
- return port === 22 ? [h, `[${h}]:22`] : [`[${h}]:${port}`];
1861
- }
1862
- async function readKnownHosts(file) {
1863
- try {
1864
- return (await readFile2(file, "utf8")).split(/\r?\n/);
1865
- } catch {
1866
- return [];
1867
- }
1868
- }
1869
- async function lookupHostKey(host, port, blob) {
1870
- const patterns = hostPatterns(host, port);
1871
- const type = keyType(blob);
1872
- const encoded = blob.toString("base64");
1873
- let sawHost = false;
1874
- const lines = [...await readKnownHosts(OPENSSH_KNOWN_HOSTS), ...await readKnownHosts(PLANCHE_KNOWN_HOSTS)];
1875
- for (const raw of lines) {
1876
- const line = raw.trim();
1877
- if (!line || line.startsWith("#")) continue;
1878
- const fields = line.split(/\s+/);
1879
- const marker = fields[0].startsWith("@") ? fields.shift() : void 0;
1880
- const [hosts, lineType, lineKey] = fields;
1881
- if (!hosts || !lineType || !lineKey) continue;
1882
- if (!knownHostsEntryMatches(hosts, patterns)) continue;
1883
- if (marker === "@revoked") {
1884
- if (lineType === type && lineKey === encoded) throw new TunnelError(`The host key of ${host}:${port} is marked @revoked in known_hosts`, 400);
1885
- continue;
1886
- }
1887
- if (marker === "@cert-authority") continue;
1888
- sawHost = true;
1889
- if (lineType === type && lineKey === encoded) return "match";
1890
- }
1891
- return sawHost ? "mismatch" : "unknown";
1892
- }
1893
- async function rememberHostKey(host, port, blob) {
1894
- await mkdir(PLANCHE_DIR, { recursive: true, mode: 448 });
1895
- const name = port === 22 ? host.toLowerCase() : `[${host.toLowerCase()}]:${port}`;
1896
- await appendFile(PLANCHE_KNOWN_HOSTS, `${name} ${keyType(blob)} ${blob.toString("base64")}
1897
- `, { mode: 384 });
1898
- }
1899
- function fingerprint(blob) {
1900
- return "SHA256:" + createHash("sha256").update(blob).digest("base64").replace(/=+$/, "");
1901
- }
1902
- async function resolveConfig(ssh) {
1903
- const cfg = await resolveSshHost(ssh.host);
1904
- if (cfg.proxyJump) {
1905
- throw new TunnelError(`ProxyJump (${cfg.proxyJump}) configured for ${ssh.host} in ~/.ssh/config is not supported`, 400);
1906
- }
1907
- if (cfg.hostName && (cfg.hostName.length > 253 || !/^[A-Za-z0-9._:[\]-]+$/.test(cfg.hostName))) {
1908
- throw new TunnelError(`HostName for ${ssh.host} in ~/.ssh/config is not a valid host name`, 400);
1909
- }
1910
- const resolved = {
1911
- ...ssh,
1912
- host: cfg.hostName ?? ssh.host,
1913
- port: ssh.port === 22 && cfg.port ? cfg.port : ssh.port,
1914
- user: ssh.user || cfg.user || ""
1915
- };
1916
- if (!resolved.user) {
1917
- throw new TunnelError(
1918
- SSH_CONFIG_DISABLED ? "ssh.user is required" : `ssh.user is required (no User for ${ssh.host} in ~/.ssh/config)`,
1919
- 400
1920
- );
1921
- }
1922
- if (resolved.auth === "key" && !resolved.privateKey) {
1923
- if (SSH_CONFIG_DISABLED) {
1924
- throw new TunnelError("ssh.privateKey is required (the agent runs with PLANCHE_SSH_CONFIG=none)", 400);
1925
- }
1926
- const candidates = cfg.identityFiles.length ? cfg.identityFiles : DEFAULT_IDENTITY_FILES;
1927
- for (const file of candidates) {
1928
- try {
1929
- resolved.privateKey = await readFile2(file, "utf8");
1930
- console.log(`[ssh] Using identity file ${file} for ${ssh.host}`);
1931
- break;
1932
- } catch {
1933
- }
1934
- }
1935
- if (!resolved.privateKey) {
1936
- throw new TunnelError(
1937
- `No private key given and none readable for ${ssh.host} (tried ${candidates.map((f) => basename3(f)).join(", ")} in ~/.ssh)`,
1938
- 400
1939
- );
1940
- }
1941
- }
1942
- return { ...resolved, identityAgent: cfg.identityAgent };
1943
- }
1944
- function toConnectConfig(ssh) {
1945
- const base = {
1946
- host: ssh.host,
1947
- port: ssh.port,
1948
- username: ssh.user,
1949
- readyTimeout: CONNECT_TIMEOUT_MS,
1950
- keepaliveInterval: 3e4
1951
- };
1952
- switch (ssh.auth) {
1953
- case "password":
1954
- return { ...base, password: ssh.password ?? "", tryKeyboard: true };
1955
- case "key":
1956
- return { ...base, privateKey: ssh.privateKey, passphrase: ssh.passphrase };
1957
- case "agent": {
1958
- const agent = process.env.SSH_AUTH_SOCK || ssh.identityAgent || (process.platform === "win32" ? "pageant" : void 0);
1959
- if (!agent) throw new TunnelError("No ssh-agent found (SSH_AUTH_SOCK is not set)", 400);
1960
- return { ...base, agent };
1961
- }
1962
- }
1963
- }
1964
- function describeSshError(err, ssh) {
1965
- const target = `${ssh.user}@${ssh.host}:${ssh.port}`;
1966
- if (err instanceof TunnelError) return err;
1967
- switch (err.level) {
1968
- case "client-authentication":
1969
- return new TunnelError(`SSH authentication failed for ${target}`, 401);
1970
- case "client-timeout":
1971
- return new TunnelError(`SSH connection to ${target} timed out`, 400);
1972
- default:
1973
- return new TunnelError(`SSH connection to ${target} failed: ${err.message}`, 400);
1974
- }
1975
- }
1976
- async function openSshTunnel(requested, target) {
1977
- const ssh = await resolveConfig(requested);
1978
- const client2 = new Client();
1979
- const connectConfig = toConnectConfig(ssh);
1980
- let verifyError;
1981
- connectConfig.hostVerifier = (key, verified) => {
1982
- lookupHostKey(ssh.host, ssh.port, key).then(async (result) => {
1983
- const fp = fingerprint(key);
1984
- if (result === "mismatch") {
1985
- verifyError = new TunnelError(
1986
- `Host key for ${ssh.host}:${ssh.port} has changed (${fp}). Remove the old entry from ~/.ssh/known_hosts or ~/.planche/known_hosts if this is expected.`,
1987
- 400
1988
- );
1989
- verified(false);
1990
- return;
1991
- }
1992
- if (result === "unknown") {
1993
- if (ssh.acceptHostKey !== fp) {
1994
- verifyError = new HostKeyError(ssh.host, ssh.port, keyType(key), fp);
1995
- verified(false);
1996
- return;
1997
- }
1998
- console.log(`[ssh] Host key for ${ssh.host}:${ssh.port} (${fp}) accepted by the user, saved to ~/.planche/known_hosts`);
1999
- await rememberHostKey(ssh.host, ssh.port, key).catch((e) => console.warn(`[ssh] Could not save host key: ${e.message}`));
2000
- }
2001
- verified(true);
2002
- }).catch((err) => {
2003
- verifyError = err instanceof TunnelError ? err : new TunnelError(`Host key verification failed: ${err instanceof Error ? err.message : String(err)}`, 400);
2004
- verified(false);
2005
- });
2006
- };
2007
- await new Promise((resolve5, reject) => {
2008
- client2.once("ready", resolve5);
2009
- client2.once("error", (err) => reject(verifyError ?? describeSshError(err, ssh)));
2010
- try {
2011
- client2.connect(connectConfig);
2012
- } catch (err) {
2013
- reject(describeSshError(err, ssh));
2014
- }
2015
- });
2016
- client2.on("error", (err) => console.error(`[ssh] ${ssh.host}:${ssh.port}: ${err.message}`));
2017
- const sockets = /* @__PURE__ */ new Set();
2018
- let forwardError;
2019
- const server = createServer((socket) => {
2020
- sockets.add(socket);
2021
- socket.once("close", () => sockets.delete(socket));
2022
- socket.on("error", () => socket.destroy());
2023
- client2.forwardOut(socket.localAddress ?? "127.0.0.1", socket.localPort ?? 0, target.host, target.port, (err, stream) => {
2024
- if (err) {
2025
- const local = /^(localhost|127\.0\.0\.1|::1)$/i.test(target.host);
2026
- const hint = local ? `"${target.host}" is resolved on the jump host itself; use the database's real address (e.g. the RDS endpoint) as the database host` : "is the database reachable from the jump host, and does it allow TCP forwarding?";
2027
- forwardError = `SSH port forwarding to ${target.host}:${target.port} failed: ${err.message} (${hint})`;
2028
- console.error(`[ssh] ${forwardError}`);
2029
- socket.destroy();
2030
- return;
2031
- }
2032
- stream.on("error", () => socket.destroy());
2033
- socket.pipe(stream).pipe(socket);
2034
- stream.once("close", () => socket.destroy());
2035
- socket.once("close", () => stream.destroy());
2036
- });
2037
- });
2038
- await new Promise((resolve5, reject) => {
2039
- server.once("error", reject);
2040
- server.listen(0, "127.0.0.1", () => resolve5());
2041
- }).catch((err) => {
2042
- client2.end();
2043
- throw new TunnelError(`Could not open local tunnel port: ${err.message}`, 400);
2044
- });
2045
- const { port } = server.address();
2046
- let closed = false;
2047
- const close = () => new Promise((resolve5) => {
2048
- if (closed) return resolve5();
2049
- closed = true;
2050
- for (const s of sockets) s.destroy();
2051
- server.close(() => {
2052
- client2.end();
2053
- resolve5();
2054
- });
2055
- });
2056
- client2.once("close", () => void close());
2057
- return {
2058
- host: "127.0.0.1",
2059
- port,
2060
- get forwardError() {
2061
- return forwardError;
2062
- },
2063
- close
2064
- };
2065
- }
2066
-
2067
- // src/usage.ts
2068
- import { mkdir as mkdir2, readFile as readFile3, rename, writeFile } from "fs/promises";
2069
- import { homedir as homedir4 } from "os";
2070
- import { join as join4 } from "path";
2071
-
2072
- // ../shared/contract.ts
2073
- var PROTOCOL_VERSION = 2;
2074
- var PRO_FEATURES = [
2075
- "ssh_tunnel",
2076
- "compare_schemas",
2077
- "compare_data",
2078
- "copy_table",
2079
- "federated",
2080
- "scratchpad",
2081
- "column_link",
2082
- "sync",
2083
- "ai_explain"
2084
- ];
2085
-
2086
- // src/usage.ts
2087
- var PLANCHE_DIR2 = join4(homedir4(), ".planche");
2088
- var USAGE_FILE = join4(PLANCHE_DIR2, "usage.json");
2089
- var WRITE_DEBOUNCE_MS = 2e3;
2090
- var KNOWN = new Set(PRO_FEATURES);
2091
- var state = null;
2092
- var loading = null;
2093
- var timer = null;
2094
- var writes = Promise.resolve();
2095
- function isProFeature(value) {
2096
- return typeof value === "string" && KNOWN.has(value);
2097
- }
2098
- var isoNow = () => (/* @__PURE__ */ new Date()).toISOString();
2099
- function empty() {
2100
- return { version: 1, since: isoNow(), features: {} };
2101
- }
2102
- function parse(raw) {
2103
- const data = JSON.parse(raw);
2104
- if (!data || typeof data !== "object") return empty();
2105
- const features = {};
2106
- for (const [key, value] of Object.entries(data.features ?? {})) {
2107
- if (!isProFeature(key) || !value || typeof value !== "object") continue;
2108
- const entry = value;
2109
- const count = Number(entry.count);
2110
- if (!Number.isFinite(count) || count < 0) continue;
2111
- features[key] = {
2112
- count: Math.floor(count),
2113
- first: typeof entry.first === "string" ? entry.first : isoNow(),
2114
- last: typeof entry.last === "string" ? entry.last : isoNow()
2115
- };
2116
- }
2117
- return { version: 1, since: typeof data.since === "string" ? data.since : isoNow(), features };
2118
- }
2119
- async function load() {
2120
- if (state) return state;
2121
- loading ??= (async () => {
2122
- try {
2123
- state = parse(await readFile3(USAGE_FILE, "utf8"));
2124
- } catch {
2125
- state = empty();
2126
- }
2127
- return state;
2128
- })();
2129
- return loading;
2130
- }
2131
- async function persist() {
2132
- if (!state) return;
2133
- const body = `${JSON.stringify(state, null, 2)}
2134
- `;
2135
- await mkdir2(PLANCHE_DIR2, { recursive: true, mode: 448 });
2136
- const tmp = `${USAGE_FILE}.${process.pid}.tmp`;
2137
- await writeFile(tmp, body, { mode: 384 });
2138
- await rename(tmp, USAGE_FILE);
2139
- }
2140
- function schedule() {
2141
- if (timer) return;
2142
- timer = setTimeout(() => {
2143
- timer = null;
2144
- void flushUsage();
2145
- }, WRITE_DEBOUNCE_MS);
2146
- timer.unref?.();
2147
- }
2148
- function recordUsage(feature) {
2149
- void (async () => {
2150
- try {
2151
- const file = await load();
2152
- const now = isoNow();
2153
- const entry = file.features[feature];
2154
- if (entry) {
2155
- entry.count += 1;
2156
- entry.last = now;
2157
- } else {
2158
- file.features[feature] = { count: 1, first: now, last: now };
2159
- }
2160
- schedule();
2161
- } catch {
2162
- }
2163
- })();
2164
- }
2165
- function flushUsage() {
2166
- if (timer) {
2167
- clearTimeout(timer);
2168
- timer = null;
2169
- }
2170
- writes = writes.then(persist).catch(() => {
2171
- });
2172
- return writes;
2173
- }
2174
- async function usageSnapshot() {
2175
- const file = await load().catch(empty);
2176
- return { success: true, since: file.since, features: { ...file.features } };
2177
- }
2178
-
2179
- // src/drivers/index.ts
2180
- var factories = {
2181
- mysql: mysqlDriverFactory,
2182
- postgres: postgresDriverFactory,
2183
- sqlite: sqliteDriverFactory,
2184
- mssql: mssqlDriverFactory,
2185
- oracle: oracleDriverFactory,
2186
- d1: d1DriverFactory,
2187
- do: doDriverFactory,
2188
- "do-remote": doRemoteDriverFactory
2189
- };
2190
- function isDriverType(value) {
2191
- return typeof value === "string" && value in factories;
2192
- }
2193
- function isDirectDriver(config) {
2194
- return config.driver === "sqlite" || config.driver === "d1" || config.driver === "do" || config.driver === "do-remote";
2195
- }
2196
- function parseDriverConfig(body) {
2197
- const driver = body.driver ?? "mysql";
2198
- if (!isDriverType(driver)) {
2199
- throw new Error(`Unsupported driver: ${String(driver)}`);
2200
- }
2201
- const config = factories[driver].parseConfig(body);
2202
- if (isDirectDriver(config)) return config;
2203
- const ssh = parseSshConfig(body.ssh);
2204
- return ssh ? { ...config, ssh } : config;
2205
- }
2206
- async function withTunnel(config, run2) {
2207
- if (isDirectDriver(config) || !config.ssh) return run2(config, void 0);
2208
- const tunnel = await openSshTunnel(config.ssh, { host: config.host, port: config.port });
2209
- recordUsage("ssh_tunnel");
2210
- try {
2211
- return await run2({ ...config, host: tunnel.host, port: tunnel.port, ssh: void 0 }, tunnel);
2212
- } catch (err) {
2213
- await tunnel.close();
2214
- throw tunnel.forwardError ? new TunnelError(tunnel.forwardError, 400) : err;
2215
- }
2216
- }
2217
- function tunneled(driver, tunnel, label) {
2218
- return {
2219
- type: driver.type,
2220
- label,
2221
- query: (sql2, database) => driver.query(sql2, database),
2222
- setReadOnly: driver.setReadOnly ? () => driver.setReadOnly() : void 0,
2223
- // Forwarded, or a tunnelled connection would report that its engine cannot
2224
- // interrupt a statement when it is only this wrapper that cannot.
2225
- cancel: driver.cancel ? () => driver.cancel() : void 0,
2226
- async close() {
2227
- try {
2228
- await driver.close();
2229
- } finally {
2230
- await tunnel.close();
2231
- }
2232
- }
2233
- };
2234
- }
2235
- function openDriver(config) {
2236
- return withTunnel(config, async (direct, tunnel) => {
2237
- const driver = await openDirect(direct);
2238
- if (!tunnel || isDirectDriver(config) || !config.ssh) return driver;
2239
- const label = `${config.user}@${config.host}:${config.port} via ssh ${config.ssh.user}@${config.ssh.host}:${config.ssh.port}`;
2240
- return tunneled(driver, tunnel, label);
2241
- });
2242
- }
2243
- function testDriver(config) {
2244
- return withTunnel(config, async (direct, tunnel) => {
2245
- try {
2246
- await testDirect(direct);
2247
- } finally {
2248
- await tunnel?.close();
2249
- }
2250
- });
2251
- }
2252
- function openDirect(config) {
2253
- switch (config.driver) {
2254
- case "mysql":
2255
- return factories.mysql.open(config);
2256
- case "postgres":
2257
- return factories.postgres.open(config);
2258
- case "sqlite":
2259
- return factories.sqlite.open(config);
2260
- case "mssql":
2261
- return factories.mssql.open(config);
2262
- case "oracle":
2263
- return factories.oracle.open(config);
2264
- case "d1":
2265
- return factories.d1.open(config);
2266
- case "do":
2267
- return factories.do.open(config);
2268
- case "do-remote":
2269
- return factories["do-remote"].open(config);
2270
- }
2271
- }
2272
- function testDirect(config) {
2273
- switch (config.driver) {
2274
- case "mysql":
2275
- return factories.mysql.test(config);
2276
- case "postgres":
2277
- return factories.postgres.test(config);
2278
- case "sqlite":
2279
- return factories.sqlite.test(config);
2280
- case "mssql":
2281
- return factories.mssql.test(config);
2282
- case "oracle":
2283
- return factories.oracle.test(config);
2284
- case "d1":
2285
- return factories.d1.test(config);
2286
- case "do":
2287
- return factories.do.test(config);
2288
- case "do-remote":
2289
- return factories["do-remote"].test(config);
2290
- }
2291
- }
2292
-
2293
- // src/gateway.ts
2294
- function loadGatewayConfig(file) {
2295
- const path = resolve2(file);
2296
- let raw;
2297
- try {
2298
- raw = JSON.parse(readFileSync(path, "utf8"));
2299
- } catch (err) {
2300
- throw new Error(`Cannot read gateway config ${path}: ${err instanceof Error ? err.message : String(err)}`);
2301
- }
2302
- const data = raw;
2303
- if (!data || typeof data !== "object" || !Array.isArray(data.profiles) || data.profiles.length === 0) {
2304
- throw new Error(`Gateway config ${path} needs a non-empty "profiles" array`);
2305
- }
2306
- const names = /* @__PURE__ */ new Set();
2307
- const profiles = data.profiles.map((entry, i) => {
2308
- const e = entry ?? {};
2309
- const name = typeof e.name === "string" ? e.name.trim() : "";
2310
- if (!name) throw new Error(`Gateway profile #${i + 1} has no "name"`);
2311
- if (names.has(name)) throw new Error(`Gateway profile name is not unique: ${name}`);
2312
- names.add(name);
2313
- const { name: _n, readOnly, ...driver } = e;
2314
- void _n;
2315
- let config;
2316
- try {
2317
- config = parseDriverConfig(driver);
2318
- } catch (err) {
2319
- throw new Error(`Gateway profile "${name}": ${err instanceof Error ? err.message : String(err)}`);
2320
- }
2321
- return { name, readOnly: readOnly !== false, config };
2322
- });
2323
- return { name: typeof data.name === "string" && data.name.trim() ? data.name.trim() : "Planche gateway", profiles };
2324
- }
2325
- function describeProfile(entry) {
2326
- const c = entry.config;
2327
- const database = "database" in c && typeof c.database === "string" ? c.database : c.driver === "oracle" ? c.serviceName : void 0;
2328
- return { name: entry.name, driver: c.driver, readOnly: entry.readOnly, ...database ? { database } : {} };
2329
- }
2330
-
2331
- // src/index.ts
2332
- import { existsSync as existsSync4 } from "fs";
2333
- import { join as join11, resolve as resolve4 } from "path";
2334
- import { fileURLToPath as fileURLToPath2 } from "url";
2335
-
2336
- // src/install.ts
2337
- import { spawnSync } from "child_process";
2338
- import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, rmSync, writeFileSync } from "fs";
2339
- import { homedir as homedir5 } from "os";
2340
- import { createInterface } from "readline/promises";
2341
- import { delimiter, dirname as dirname3, join as join5, resolve as resolve3 } from "path";
2342
- import { fileURLToPath } from "url";
2343
- var MANIFEST = "install.json";
2344
- var PLANCHE_DIR3 = join5(homedir5(), ".planche");
2345
- var UPDATE_CACHE = join5(PLANCHE_DIR3, "update-check.json");
2346
- var REGISTRY = "https://registry.npmjs.org/planche/latest";
2347
- var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
2348
- var cached = null;
2349
- function locate() {
2350
- if (cached) return cached;
2351
- const candidates = [];
2352
- if (process.env.PLANCHE_INSTALL_ROOT) candidates.push(resolve3(process.env.PLANCHE_INSTALL_ROOT));
2353
- let dir = dirname3(fileURLToPath(import.meta.url));
2354
- for (let i = 0; i < 6; i += 1) {
2355
- candidates.push(dir);
2356
- const parent = dirname3(dir);
2357
- if (parent === dir) break;
2358
- dir = parent;
2359
- }
2360
- for (const root of candidates) {
2361
- try {
2362
- const raw = readFileSync2(join5(root, MANIFEST), "utf8");
2363
- const manifest = JSON.parse(raw);
2364
- if (manifest && manifest.version === 1) {
2365
- cached = { root, manifest };
2366
- return cached;
2367
- }
2368
- } catch {
2369
- }
2370
- }
2371
- cached = { root: null, manifest: null };
2372
- return cached;
56
+ cached = { root: null, manifest: null };
57
+ return cached;
2373
58
  }
2374
59
  function installKind() {
2375
- return locate().manifest ? "standalone" : "npx";
2376
- }
2377
- function abiWarning() {
2378
- const { manifest } = locate();
2379
- if (!manifest?.modules || manifest.modules === process.versions.modules) return null;
2380
- return `This install was built for Node ABI ${manifest.modules}, but it is running on ${process.version} (ABI ${process.versions.modules}).
2381
- SQLite connections will fail until the native bindings are rebuilt \u2014 run: planche upgrade`;
2382
- }
2383
- function npmCommand(manifest) {
2384
- const node = manifest.node && existsSync2(manifest.node) ? manifest.node : process.execPath;
2385
- if (manifest.npmCli && existsSync2(manifest.npmCli)) return { cmd: node, args: [manifest.npmCli], shell: false, node };
2386
- if (manifest.npm && existsSync2(manifest.npm)) {
2387
- return { cmd: manifest.npm, args: [], shell: process.platform === "win32", node };
2388
- }
2389
- return null;
2390
- }
2391
- function npmEnv(node) {
2392
- const env = { ...process.env };
2393
- const key = Object.keys(env).find((k) => k.toUpperCase() === "PATH") ?? "PATH";
2394
- env[key] = `${dirname3(node)}${delimiter}${env[key] ?? ""}`;
2395
- return env;
2396
- }
2397
- var VERSION_SPEC = /^(latest|next|\d+\.\d+\.\d+(-[0-9A-Za-z.]+)?)$/;
2398
- function installedVersion(prefix) {
2399
- try {
2400
- const pkg = JSON.parse(readFileSync2(join5(prefix, "node_modules", "planche", "package.json"), "utf8"));
2401
- return pkg.version ?? null;
2402
- } catch {
2403
- return null;
2404
- }
2405
- }
2406
- function runUpgrade(target) {
2407
- if (!VERSION_SPEC.test(target)) {
2408
- console.error(`Not a version: ${target}. Use \`planche upgrade\` or \`planche upgrade 1.2.3\`.`);
2409
- return 2;
2410
- }
2411
- const { root, manifest } = locate();
2412
- if (!root || !manifest) {
2413
- console.error("This copy of planche was not installed by planche.dev/install.sh.");
2414
- console.error("Run the latest version with: npx planche@latest");
2415
- return 1;
2416
- }
2417
- const npm = npmCommand(manifest);
2418
- if (!npm) {
2419
- console.error(`Cannot find npm for this install (see ${join5(root, MANIFEST)}).`);
2420
- console.error("Re-run the installer: curl -fsSL https://planche.dev/install.sh | sh");
2421
- return 1;
2422
- }
2423
- const prefix = join5(root, "pkg");
2424
- const before = installedVersion(prefix);
2425
- console.log(`Installing planche@${target} into ${prefix} ...`);
2426
- const result = spawnSync(
2427
- npm.cmd,
2428
- [
2429
- ...npm.args,
2430
- "install",
2431
- "--prefix",
2432
- prefix,
2433
- "--omit=dev",
2434
- "--no-audit",
2435
- "--no-fund",
2436
- "--loglevel=error",
2437
- // Without this npm resolves `latest` from its cached registry document,
2438
- // which is exactly wrong in the minutes after a release: it reports "up
2439
- // to date", exits 0, and leaves the old version in place.
2440
- "--prefer-online",
2441
- `planche@${target}`
2442
- ],
2443
- { stdio: "inherit", shell: npm.shell, env: npmEnv(npm.node) }
2444
- );
2445
- if (result.status !== 0) {
2446
- console.error("Upgrade failed. Re-run the installer to repair the install:");
2447
- console.error(" curl -fsSL https://planche.dev/install.sh | sh");
2448
- return result.status ?? 1;
2449
- }
2450
- const installed = installedVersion(prefix) ?? target;
2451
- try {
2452
- writeFileSync(join5(root, MANIFEST), `${JSON.stringify({ ...manifest, modules: process.versions.modules, installedAt: (/* @__PURE__ */ new Date()).toISOString() }, null, 2)}
2453
- `);
2454
- } catch {
2455
- }
2456
- cached = null;
2457
- clearUpdateCache();
2458
- console.log(before === installed ? `planche ${installed} was already installed; nothing changed.` : `planche ${installed} installed. Start it with: planche`);
2459
- return 0;
2460
- }
2461
- async function runUninstall(assumeYes) {
2462
- const { root, manifest } = locate();
2463
- if (!root || !manifest) {
2464
- console.error("This copy of planche was not installed by planche.dev/install.sh; there is nothing to uninstall.");
2465
- console.error("An `npx planche` download lives in the npm cache and is cleared with: npm cache clean --force");
2466
- return 1;
2467
- }
2468
- const targets = [root, ...manifest.bin && existsSync2(manifest.bin) ? [manifest.bin] : []];
2469
- console.log("This will remove:");
2470
- for (const t of targets) console.log(` ${t}`);
2471
- console.log(`
2472
- Your settings stay: ${PLANCHE_DIR3} (known_hosts, usage counts) is left untouched.`);
2473
- if (!assumeYes) {
2474
- if (!process.stdin.isTTY) {
2475
- console.error("\nRefusing to delete without a confirmation. Re-run with: planche uninstall --yes");
2476
- return 1;
2477
- }
2478
- const rl = createInterface({ input: process.stdin, output: process.stdout });
2479
- const answer = (await rl.question("\nRemove them? [y/N] ")).trim().toLowerCase();
2480
- rl.close();
2481
- if (answer !== "y" && answer !== "yes") {
2482
- console.log("Nothing was removed.");
2483
- return 1;
2484
- }
2485
- }
2486
- for (const t of targets) {
2487
- try {
2488
- rmSync(t, { recursive: true, force: true });
2489
- } catch (err) {
2490
- console.error(`Could not remove ${t}: ${err instanceof Error ? err.message : String(err)}`);
2491
- return 1;
2492
- }
2493
- }
2494
- console.log("planche has been uninstalled.");
2495
- return 0;
2496
- }
2497
- function readUpdateCache() {
2498
- try {
2499
- const data = JSON.parse(readFileSync2(UPDATE_CACHE, "utf8"));
2500
- return typeof data.checkedAt === "number" && typeof data.latest === "string" ? data : null;
2501
- } catch {
2502
- return null;
2503
- }
2504
- }
2505
- function writeUpdateCache(latest) {
2506
- try {
2507
- mkdirSync(PLANCHE_DIR3, { recursive: true, mode: 448 });
2508
- writeFileSync(UPDATE_CACHE, `${JSON.stringify({ checkedAt: Date.now(), latest })}
2509
- `, { mode: 384 });
2510
- } catch {
2511
- }
2512
- }
2513
- function clearUpdateCache() {
2514
- try {
2515
- rmSync(UPDATE_CACHE, { force: true });
2516
- } catch {
2517
- }
2518
- }
2519
- function isNewer(candidate, current) {
2520
- const parse2 = (v) => /^\d+\.\d+\.\d+$/.test(v) ? v.split(".").map(Number) : null;
2521
- const a = parse2(candidate);
2522
- const b = parse2(current);
2523
- if (!a || !b) return false;
2524
- for (let i = 0; i < 3; i += 1) {
2525
- if (a[i] !== b[i]) return a[i] > b[i];
2526
- }
2527
- return false;
2528
- }
2529
- async function updateNotice(current) {
2530
- if (installKind() !== "standalone") return null;
2531
- if (process.env.PLANCHE_NO_UPDATE_CHECK) return null;
2532
- const cache = readUpdateCache();
2533
- let latest = cache && Date.now() - cache.checkedAt < CHECK_INTERVAL_MS ? cache.latest : null;
2534
- if (!latest) {
2535
- const controller = new AbortController();
2536
- const timer2 = setTimeout(() => controller.abort(), 2500);
2537
- try {
2538
- const response = await fetch(REGISTRY, { signal: controller.signal, headers: { accept: "application/json" } });
2539
- if (!response.ok) return null;
2540
- const data = await response.json();
2541
- if (typeof data.version !== "string" || !/^[\w.+-]{1,32}$/.test(data.version)) return null;
2542
- latest = data.version;
2543
- writeUpdateCache(latest);
2544
- } catch {
2545
- return null;
2546
- } finally {
2547
- clearTimeout(timer2);
2548
- }
2549
- }
2550
- return isNewer(latest, current) ? `planche ${latest} is available (you have ${current}) \u2014 update with: planche upgrade` : null;
2551
- }
2552
-
2553
- // src/ai-cli.ts
2554
- import { execFile as execFileCb2, spawn } from "child_process";
2555
- import { mkdtemp, readFile as readFile5, rm, writeFile as writeFile3 } from "fs/promises";
2556
- import { tmpdir } from "os";
2557
- import { join as join8 } from "path";
2558
- import { promisify as promisify2 } from "util";
2559
-
2560
- // src/ai-config.ts
2561
- import { chmod, mkdir as mkdir3, readFile as readFile4, rename as rename2, unlink, writeFile as writeFile2 } from "fs/promises";
2562
- import { homedir as homedir7 } from "os";
2563
- import { join as join7 } from "path";
2564
-
2565
- // ../shared/ai.ts
2566
- var AI_DEFAULT_MODEL = {
2567
- // Anthropic's most capable general model. Explaining a failure is the paid
2568
- // half of the product, so the default is not chosen to be cheap.
2569
- anthropic: "claude-opus-5",
2570
- // Same API, same models; only where the credential comes from differs.
2571
- "anthropic-profile": "claude-opus-5",
2572
- openai: "gpt-4o",
2573
- gemini: "gemini-2.5-flash",
2574
- // Not a model but the command to run: the CLI decides the model itself.
2575
- cli: "claude"
2576
- };
2577
-
2578
- // src/ai-anthropic.ts
2579
- import { execFile as execFileCb } from "child_process";
2580
- import { access } from "fs/promises";
2581
- import { homedir as homedir6 } from "os";
2582
- import { join as join6 } from "path";
2583
- import { promisify } from "util";
2584
- import Anthropic from "@anthropic-ai/sdk";
2585
- function toMessages(turns) {
2586
- const out = [];
2587
- for (const turn of turns) {
2588
- if (turn.role === "user") {
2589
- out.push({ role: "user", content: turn.content });
2590
- continue;
2591
- }
2592
- if (turn.role === "assistant") {
2593
- const blocks = [];
2594
- if (turn.content.trim()) blocks.push({ type: "text", text: turn.content });
2595
- for (const call of turn.toolCalls ?? []) {
2596
- blocks.push({ type: "tool_use", id: call.id, name: call.name, input: call.input });
2597
- }
2598
- if (blocks.length > 0) out.push({ role: "assistant", content: blocks });
2599
- continue;
2600
- }
2601
- const result = {
2602
- type: "tool_result",
2603
- tool_use_id: turn.toolCallId,
2604
- content: turn.content,
2605
- ...turn.isError ? { is_error: true } : {}
2606
- };
2607
- const last = out[out.length - 1];
2608
- if (last && last.role === "user" && Array.isArray(last.content)) {
2609
- last.content.push(result);
2610
- } else {
2611
- out.push({ role: "user", content: [result] });
2612
- }
2613
- }
2614
- return out;
2615
- }
2616
- var execFile = promisify(execFileCb);
2617
- async function anthropicProfileAvailable() {
2618
- try {
2619
- await execFile("ant", ["auth", "status"], { timeout: 1e4 });
2620
- return true;
2621
- } catch {
2622
- try {
2623
- await access(join6(homedir6(), ".config", "anthropic"));
2624
- return true;
2625
- } catch {
2626
- return false;
2627
- }
2628
- }
2629
- }
2630
- async function runAnthropic(config, req, signal) {
2631
- const client2 = new Anthropic({
2632
- ...config.key ? { apiKey: config.key } : {},
2633
- ...config.baseUrl ? { baseURL: config.baseUrl } : {}
2634
- });
2635
- const tools2 = (req.tools ?? []).map((tool) => ({
2636
- name: tool.name,
2637
- description: tool.description,
2638
- input_schema: tool.inputSchema
2639
- }));
2640
- try {
2641
- const response = await client2.messages.create({
2642
- model: config.model,
2643
- max_tokens: MAX_TOKENS,
2644
- system: req.system,
2645
- messages: toMessages(turnsOf(req)),
2646
- ...tools2.length > 0 ? { tools: tools2 } : {}
2647
- }, { signal });
2648
- const text = response.content.filter((block) => block.type === "text").map((block) => block.text).join("");
2649
- const toolCalls = response.content.filter((block) => block.type === "tool_use").map((block) => ({ id: block.id, name: block.name, input: block.input ?? {} }));
2650
- if (!text && toolCalls.length === 0) throw new AiError(502, "The model returned nothing.");
2651
- return {
2652
- text,
2653
- toolCalls,
2654
- stop: response.stop_reason === "tool_use" ? "tool_use" : response.stop_reason === "max_tokens" ? "length" : "end"
2655
- };
2656
- } catch (err) {
2657
- if (err instanceof AiError) throw err;
2658
- const noCredential = 'This machine has no Anthropic credential the SDK could use. Run "ant auth login", or set a key instead.';
2659
- if (err instanceof Anthropic.AuthenticationError) {
2660
- throw new AiError(401, config.key ? "The AI key was rejected." : noCredential);
2661
- }
2662
- if (!config.key && err instanceof Error && /resolve authentication/i.test(err.message)) {
2663
- throw new AiError(401, noCredential);
2664
- }
2665
- if (err instanceof Anthropic.RateLimitError) throw new AiError(429, "The provider is rate limiting this key.");
2666
- if (err instanceof Anthropic.BadRequestError) throw new AiError(400, err.message);
2667
- if (err instanceof Anthropic.APIError) throw new AiError(502, `The provider answered ${err.status}: ${err.message}`);
2668
- throw new AiError(502, err instanceof Error ? err.message : String(err));
2669
- }
2670
- }
2671
-
2672
- // src/ai-config.ts
2673
- var PLANCHE_DIR4 = join7(homedir7(), ".planche");
2674
- var AI_FILE = join7(PLANCHE_DIR4, "ai.json");
2675
- function envConfig() {
2676
- const key = process.env.PLANCHE_AI_KEY?.trim() || process.env.ANTHROPIC_API_KEY?.trim() || process.env.OPENAI_API_KEY?.trim() || process.env.GEMINI_API_KEY?.trim();
2677
- if (!key) return null;
2678
- const named = process.env.PLANCHE_AI_PROVIDER?.trim();
2679
- const provider = named === "openai" || named === "anthropic" || named === "gemini" || named === "cli" || named === "anthropic-profile" ? named : process.env.GEMINI_API_KEY?.trim() ? "gemini" : process.env.OPENAI_API_KEY?.trim() && !process.env.ANTHROPIC_API_KEY?.trim() ? "openai" : "anthropic";
2680
- return {
2681
- provider,
2682
- model: process.env.PLANCHE_AI_MODEL?.trim() || AI_DEFAULT_MODEL[provider],
2683
- baseUrl: process.env.PLANCHE_AI_BASE_URL?.trim() || void 0,
2684
- key
2685
- };
2686
- }
2687
- var isKeyless = (provider) => provider === "cli" || provider === "anthropic-profile";
2688
- var session = null;
2689
- var cached2;
2690
- async function fileConfig() {
2691
- if (cached2 !== void 0) return cached2;
2692
- try {
2693
- const raw = JSON.parse(await readFile4(AI_FILE, "utf8"));
2694
- const provider = raw.provider === "openai" || raw.provider === "gemini" || raw.provider === "cli" || raw.provider === "anthropic-profile" ? raw.provider : "anthropic";
2695
- const key = typeof raw.key === "string" ? raw.key : "";
2696
- cached2 = key || isKeyless(provider) ? {
2697
- provider,
2698
- model: typeof raw.model === "string" && raw.model ? raw.model : AI_DEFAULT_MODEL[provider],
2699
- baseUrl: typeof raw.baseUrl === "string" && raw.baseUrl ? raw.baseUrl : void 0,
2700
- key
2701
- } : null;
2702
- } catch {
2703
- cached2 = null;
2704
- }
2705
- return cached2;
2706
- }
2707
- async function aiConfig() {
2708
- return envConfig() ?? session ?? await fileConfig();
2709
- }
2710
- async function aiConfigView() {
2711
- const env = envConfig();
2712
- const file = env ? null : await fileConfig();
2713
- const config = env ?? session ?? file;
2714
- const storage = env ? "environment" : session ? "session" : file ? "file" : "none";
2715
- return {
2716
- configured: config !== null,
2717
- provider: config?.provider ?? "anthropic",
2718
- model: config?.model ?? AI_DEFAULT_MODEL.anthropic,
2719
- baseUrl: config?.baseUrl,
2720
- fromEnvironment: env !== null,
2721
- storage,
2722
- path: AI_FILE,
2723
- clis: await installedClis(),
2724
- anthropicProfile: await anthropicProfileAvailable()
2725
- };
2726
- }
2727
- function defaultModelFor(provider) {
2728
- return AI_DEFAULT_MODEL[provider];
2729
- }
2730
- async function saveAiConfig(config, remember) {
2731
- if (!remember && !isKeyless(config.provider)) {
2732
- session = config;
2733
- await forgetStoredConfig();
2734
- return false;
2735
- }
2736
- session = null;
2737
- await mkdir3(PLANCHE_DIR4, { recursive: true });
2738
- const temp = `${AI_FILE}.${process.pid}.tmp`;
2739
- await writeFile2(temp, JSON.stringify(config, null, 2), { mode: 384 });
2740
- await chmod(temp, 384);
2741
- await rename2(temp, AI_FILE);
2742
- cached2 = config;
2743
- return true;
2744
- }
2745
- async function forgetStoredConfig() {
2746
- cached2 = null;
2747
- try {
2748
- await unlink(AI_FILE);
2749
- } catch {
2750
- }
2751
- }
2752
- async function forgetAiConfig() {
2753
- session = null;
2754
- await forgetStoredConfig();
2755
- }
2756
- var aiConfigPath = AI_FILE;
2757
-
2758
- // src/ai-gemini.ts
2759
- var DEFAULT_BASE = "https://generativelanguage.googleapis.com/v1beta";
2760
- function toContents(turns) {
2761
- const out = [];
2762
- for (const turn of turns) {
2763
- if (turn.role === "user") {
2764
- out.push({ role: "user", parts: [{ text: turn.content }] });
2765
- } else if (turn.role === "assistant") {
2766
- const parts = [];
2767
- if (turn.content.trim()) parts.push({ text: turn.content });
2768
- for (const call of turn.toolCalls ?? []) parts.push({ functionCall: { name: call.name, args: call.input } });
2769
- if (parts.length > 0) out.push({ role: "model", parts });
2770
- } else {
2771
- const part = {
2772
- functionResponse: { name: turn.toolCallId, response: { result: turn.content, ...turn.isError ? { error: true } : {} } }
2773
- };
2774
- const last = out[out.length - 1];
2775
- if (last && last.role === "user" && last.parts.every((p) => p.functionResponse)) last.parts.push(part);
2776
- else out.push({ role: "user", parts: [part] });
2777
- }
2778
- }
2779
- return out;
2780
- }
2781
- async function runGemini(config, req, signal) {
2782
- const base = (config.baseUrl || DEFAULT_BASE).replace(/\/+$/, "");
2783
- const url = `${base}/models/${encodeURIComponent(config.model)}:generateContent?key=${encodeURIComponent(config.key)}`;
2784
- const tools2 = req.tools ?? [];
2785
- let response;
2786
- try {
2787
- response = await fetch(url, {
2788
- method: "POST",
2789
- signal,
2790
- headers: { "content-type": "application/json" },
2791
- body: JSON.stringify({
2792
- systemInstruction: { parts: [{ text: req.system }] },
2793
- contents: toContents(turnsOf(req)),
2794
- ...tools2.length > 0 ? {
2795
- tools: [
2796
- {
2797
- functionDeclarations: tools2.map((tool) => ({
2798
- name: tool.name,
2799
- description: tool.description,
2800
- parameters: tool.inputSchema
2801
- }))
2802
- }
2803
- ]
2804
- } : {},
2805
- generationConfig: {
2806
- // A JSON mime type and function calling cannot both be asked for.
2807
- ...tools2.length > 0 ? {} : { responseMimeType: "application/json" },
2808
- maxOutputTokens: MAX_TOKENS
2809
- }
2810
- })
2811
- });
2812
- } catch (err) {
2813
- throw new AiError(502, err instanceof Error ? err.message : String(err));
2814
- }
2815
- const data = await response.json().catch(() => null);
2816
- if (!response.ok) {
2817
- const message = data?.error?.message ?? `${response.status} ${response.statusText}`;
2818
- if (response.status === 400 || response.status === 401 || response.status === 403) {
2819
- throw new AiError(401, `The AI key was rejected: ${message}`);
2820
- }
2821
- if (response.status === 429) throw new AiError(429, "The provider is rate limiting this key.");
2822
- throw new AiError(502, `The provider answered ${response.status}: ${message}`);
2823
- }
2824
- const candidate = data?.candidates?.[0];
2825
- const parts = candidate?.content?.parts ?? [];
2826
- const text = parts.map((p) => p.text ?? "").join("");
2827
- const toolCalls = parts.filter((p) => typeof p.functionCall?.name === "string").map((p) => ({ id: p.functionCall.name, name: p.functionCall.name, input: p.functionCall.args ?? {} }));
2828
- if (!text && toolCalls.length === 0) throw new AiError(502, "The model returned nothing.");
2829
- return {
2830
- text,
2831
- toolCalls,
2832
- stop: toolCalls.length > 0 ? "tool_use" : candidate?.finishReason === "MAX_TOKENS" ? "length" : "end"
2833
- };
2834
- }
2835
-
2836
- // src/ai-openai.ts
2837
- import OpenAI from "openai";
2838
- function toMessages2(system, turns) {
2839
- const out = [{ role: "system", content: system }];
2840
- for (const turn of turns) {
2841
- if (turn.role === "user") {
2842
- out.push({ role: "user", content: turn.content });
2843
- } else if (turn.role === "assistant") {
2844
- const calls = turn.toolCalls ?? [];
2845
- out.push({
2846
- role: "assistant",
2847
- content: turn.content || null,
2848
- ...calls.length > 0 ? {
2849
- tool_calls: calls.map((call) => ({
2850
- id: call.id,
2851
- type: "function",
2852
- function: { name: call.name, arguments: JSON.stringify(call.input) }
2853
- }))
2854
- } : {}
2855
- });
2856
- } else {
2857
- out.push({ role: "tool", tool_call_id: turn.toolCallId, content: turn.content });
2858
- }
2859
- }
2860
- return out;
2861
- }
2862
- function parseArguments(raw) {
2863
- try {
2864
- const parsed = JSON.parse(raw || "{}");
2865
- return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
2866
- } catch {
2867
- return {};
2868
- }
2869
- }
2870
- async function runOpenAI(config, req, signal) {
2871
- const client2 = new OpenAI({ apiKey: config.key, ...config.baseUrl ? { baseURL: config.baseUrl } : {} });
2872
- const tools2 = (req.tools ?? []).map((tool) => ({
2873
- type: "function",
2874
- function: { name: tool.name, description: tool.description, parameters: tool.inputSchema }
2875
- }));
2876
- try {
2877
- const response = await client2.chat.completions.create({
2878
- model: config.model,
2879
- max_completion_tokens: MAX_TOKENS,
2880
- // JSON mode and tools are mutually exclusive here: forcing a JSON object
2881
- // leaves the model no way to answer with a call. The one-shot prompts
2882
- // ask for JSON, a tool run does not.
2883
- ...tools2.length > 0 ? { tools: tools2 } : { response_format: { type: "json_object" } },
2884
- messages: toMessages2(req.system, turnsOf(req))
2885
- }, { signal });
2886
- const choice = response.choices[0];
2887
- const text = choice?.message?.content ?? "";
2888
- const toolCalls = (choice?.message?.tool_calls ?? []).filter((call) => call.type === "function").map((call) => ({ id: call.id, name: call.function.name, input: parseArguments(call.function.arguments) }));
2889
- if (!text && toolCalls.length === 0) throw new AiError(502, "The model returned nothing.");
2890
- return {
2891
- text,
2892
- toolCalls,
2893
- stop: choice?.finish_reason === "tool_calls" ? "tool_use" : choice?.finish_reason === "length" ? "length" : "end"
2894
- };
2895
- } catch (err) {
2896
- if (err instanceof AiError) throw err;
2897
- if (err instanceof OpenAI.APIError) {
2898
- const status = err.status === 401 || err.status === 429 || err.status === 400 ? err.status : 502;
2899
- throw new AiError(status, err.status === 401 ? "The AI key was rejected." : err.message);
2900
- }
2901
- throw new AiError(502, err instanceof Error ? err.message : String(err));
2902
- }
2903
- }
2904
-
2905
- // src/ai-run.ts
2906
- var AiError = class extends Error {
2907
- constructor(status, message) {
2908
- super(message);
2909
- this.status = status;
2910
- this.name = "AiError";
2911
- }
2912
- status;
2913
- };
2914
- var MAX_TOKENS = 8e3;
2915
- function turnsOf(req) {
2916
- if (req.messages && req.messages.length > 0) return req.messages;
2917
- return [{ role: "user", content: req.user ?? "" }];
2918
- }
2919
- async function runPrompt(req, signal) {
2920
- const config = await aiConfig();
2921
- if (!config) throw new AiError(501, "No AI key is configured on this agent.");
2922
- const model = req.model?.trim() || config.model;
2923
- const run2 = config.provider === "cli" ? runCli : config.provider === "openai" ? runOpenAI : config.provider === "gemini" ? runGemini : runAnthropic;
2924
- const result = await run2({ ...config, model }, req, signal);
2925
- return {
2926
- success: true,
2927
- text: result.text,
2928
- model,
2929
- ...result.toolCalls.length > 0 ? { toolCalls: result.toolCalls } : {},
2930
- stop: result.stop
2931
- };
2932
- }
2933
-
2934
- // src/ai-cli.ts
2935
- var execFile2 = promisify2(execFileCb2);
2936
- var CLI_COMMANDS = ["claude", "codex"];
2937
- var TIMEOUT_MS = 18e4;
2938
- var mcp = null;
2939
- function configureCliBridge(url, token) {
2940
- mcp = { url, token };
2941
- }
2942
- function mcpUrl() {
2943
- return mcp?.url ?? null;
2944
- }
2945
- function isCliCommand(name) {
2946
- return CLI_COMMANDS.includes(name);
2947
- }
2948
- async function installedClis() {
2949
- const found = [];
2950
- for (const name of CLI_COMMANDS) {
2951
- try {
2952
- await execFile2(name, ["--version"], { timeout: 1e4 });
2953
- found.push(name);
2954
- } catch {
2955
- }
2956
- }
2957
- return found;
2958
- }
2959
- var STOP_GRACE_MS = 3e3;
2960
- function run(command, args, prompt, signal) {
2961
- return new Promise((resolve5, reject) => {
2962
- if (signal?.aborted) {
2963
- reject(new AiError(499, "Stopped."));
2964
- return;
2965
- }
2966
- const child = spawn(command, args, { stdio: ["pipe", "pipe", "pipe"] });
2967
- let out = "";
2968
- let err = "";
2969
- let stopped = false;
2970
- const timer2 = setTimeout(() => {
2971
- child.kill("SIGKILL");
2972
- reject(new AiError(504, `${command} did not answer within ${TIMEOUT_MS / 1e3}s.`));
2973
- }, TIMEOUT_MS);
2974
- const onAbort = () => {
2975
- stopped = true;
2976
- child.kill("SIGTERM");
2977
- setTimeout(() => child.kill("SIGKILL"), STOP_GRACE_MS).unref();
2978
- };
2979
- signal?.addEventListener("abort", onAbort, { once: true });
2980
- const done = () => {
2981
- clearTimeout(timer2);
2982
- signal?.removeEventListener("abort", onAbort);
2983
- };
2984
- child.stdout.on("data", (d) => out += d);
2985
- child.stderr.on("data", (d) => err += d);
2986
- child.on("error", (e) => {
2987
- done();
2988
- reject(new AiError(501, `${command} could not be started: ${e.message}`));
2989
- });
2990
- child.on("close", (code) => {
2991
- done();
2992
- if (stopped) reject(new AiError(499, "Stopped."));
2993
- else if (code === 0) resolve5(out);
2994
- else reject(new AiError(502, `${command} exited ${code}: ${err.trim().slice(0, 400) || "no output"}`));
2995
- });
2996
- child.stdin.end(prompt);
2997
- });
2998
- }
2999
- function unwrap(text) {
3000
- const fenced = text.trim().match(/^```(?:json)?\s*\n([\s\S]*?)\n```$/);
3001
- return (fenced ? fenced[1] : text).trim();
3002
- }
3003
- async function runCli(config, req, signal) {
3004
- const command = config.model.trim();
3005
- const wantsTools = (req.tools?.length ?? 0) > 0;
3006
- if (wantsTools && command !== "claude") {
3007
- throw new AiError(400, `${command} cannot be given Planche's tools. Use the claude CLI for the AI panel, or a provider with an API key.`);
3008
- }
3009
- if (wantsTools && !mcp) {
3010
- throw new AiError(501, "This agent cannot offer its tools to a CLI. Restart it, or use a provider with an API key.");
3011
- }
3012
- if (!isCliCommand(command)) {
3013
- throw new AiError(400, `Unknown CLI "${command}". Use one of: ${CLI_COMMANDS.join(", ")}.`);
3014
- }
3015
- const turns = turnsOf(req);
3016
- const prompt = [req.system, "", turns.map((t) => "content" in t ? t.content : "").join("\n\n")].join("\n");
3017
- let text;
3018
- if (command === "claude") {
3019
- if (!wantsTools) {
3020
- text = await run("claude", ["-p"], prompt, signal);
3021
- } else {
3022
- const dir = await mkdtemp(join8(tmpdir(), "planche-mcp-"));
3023
- const file = join8(dir, "mcp.json");
3024
- try {
3025
- await writeFile3(
3026
- file,
3027
- JSON.stringify({
3028
- mcpServers: {
3029
- planche: { type: "http", url: mcp.url, headers: { Authorization: `Bearer ${mcp.token}` } }
3030
- }
3031
- }),
3032
- { mode: 384 }
3033
- );
3034
- text = await run("claude", ["-p", "--strict-mcp-config", "--mcp-config", file], prompt, signal);
3035
- } finally {
3036
- await rm(dir, { recursive: true, force: true });
3037
- }
3038
- }
3039
- } else {
3040
- const dir = await mkdtemp(join8(tmpdir(), "planche-cli-"));
3041
- const file = join8(dir, "answer.txt");
3042
- try {
3043
- await run("codex", ["exec", "--skip-git-repo-check", "-o", file, "-"], prompt, signal);
3044
- text = await readFile5(file, "utf8");
3045
- } finally {
3046
- await rm(dir, { recursive: true, force: true });
3047
- }
3048
- }
3049
- const answer = unwrap(text);
3050
- if (!answer) throw new AiError(502, `${command} returned nothing.`);
3051
- return { text: answer, toolCalls: [], stop: "end" };
3052
- }
3053
-
3054
- // src/mcp-token.ts
3055
- import { chmod as chmod2, mkdir as mkdir4, readFile as readFile6, rm as rm2, writeFile as writeFile4 } from "fs/promises";
3056
- import { randomBytes } from "crypto";
3057
- import { homedir as homedir8 } from "os";
3058
- import { join as join9 } from "path";
3059
- var DIR = join9(homedir8(), ".planche");
3060
- var FILE = join9(DIR, "mcp-token");
3061
- var mcpTokenPath = FILE;
3062
- var cached3 = null;
3063
- async function readMcpToken() {
3064
- if (cached3) return cached3;
3065
- try {
3066
- const text = (await readFile6(FILE, "utf8")).trim();
3067
- cached3 = text || null;
3068
- } catch {
3069
- cached3 = null;
3070
- }
3071
- return cached3;
3072
- }
3073
- async function ensureMcpToken() {
3074
- const existing = await readMcpToken();
3075
- if (existing) return existing;
3076
- const token = randomBytes(32).toString("base64url");
3077
- await mkdir4(DIR, { recursive: true, mode: 448 });
3078
- await writeFile4(FILE, `${token}
3079
- `, { mode: 384 });
3080
- await chmod2(FILE, 384);
3081
- cached3 = token;
3082
- return token;
3083
- }
3084
- async function forgetMcpToken() {
3085
- cached3 = null;
3086
- await rm2(FILE, { force: true });
3087
- }
3088
-
3089
- // src/server.ts
3090
- import express from "express";
3091
- import { createHash as createHash2, randomUUID as randomUUID2, timingSafeEqual as timingSafeEqual2 } from "crypto";
3092
-
3093
- // src/bridge.ts
3094
- var CALL_TIMEOUT_MS = 12e4;
3095
- var client = null;
3096
- var tools = [];
3097
- var pending = /* @__PURE__ */ new Map();
3098
- var nextId = 0;
3099
- function bridgeTools() {
3100
- return tools;
3101
- }
3102
- function attachBridge(res) {
3103
- client?.end();
3104
- client = res;
3105
- res.on("close", () => {
3106
- if (client !== res) return;
3107
- client = null;
3108
- tools = [];
3109
- });
60
+ return locate().manifest ? "standalone" : "npx";
3110
61
  }
3111
- function setBridgeTools(next) {
3112
- tools = next;
62
+ function abiWarning() {
63
+ const { manifest } = locate();
64
+ if (!manifest?.modules || manifest.modules === process.versions.modules) return null;
65
+ return `This install was built for Node ABI ${manifest.modules}, but it is running on ${process.version} (ABI ${process.versions.modules}).
66
+ SQLite connections will fail until the native bindings are rebuilt \u2014 run: planche upgrade`;
3113
67
  }
3114
- function callThroughBridge(name, input) {
3115
- const target = client;
3116
- if (!target) {
3117
- return Promise.resolve({ content: "Planche is not open in a browser, so its tools cannot run.", isError: true });
68
+ function npmCommand(manifest) {
69
+ const node = manifest.node && existsSync(manifest.node) ? manifest.node : process.execPath;
70
+ if (manifest.npmCli && existsSync(manifest.npmCli)) return { cmd: node, args: [manifest.npmCli], shell: false, node };
71
+ if (manifest.npm && existsSync(manifest.npm)) {
72
+ return { cmd: manifest.npm, args: [], shell: process.platform === "win32", node };
3118
73
  }
3119
- const id = `call-${++nextId}`;
3120
- return new Promise((resolve5) => {
3121
- const timer2 = setTimeout(() => {
3122
- pending.delete(id);
3123
- resolve5({ content: `The app did not answer "${name}" within ${CALL_TIMEOUT_MS / 1e3}s.`, isError: true });
3124
- }, CALL_TIMEOUT_MS);
3125
- pending.set(id, { resolve: resolve5, timer: timer2 });
3126
- target.write(`data: ${JSON.stringify({ id, name, input })}
3127
-
3128
- `);
3129
- });
74
+ return null;
3130
75
  }
3131
- function resolveBridgeCall(id, content, isError) {
3132
- const waiting = pending.get(id);
3133
- if (!waiting) return;
3134
- pending.delete(id);
3135
- clearTimeout(waiting.timer);
3136
- waiting.resolve({ content, isError });
76
+ function npmEnv(node) {
77
+ const env = { ...process.env };
78
+ const key = Object.keys(env).find((k) => k.toUpperCase() === "PATH") ?? "PATH";
79
+ env[key] = `${dirname(node)}${delimiter}${env[key] ?? ""}`;
80
+ return env;
3137
81
  }
3138
-
3139
- // src/mcp.ts
3140
- var FALLBACK_PROTOCOL = "2025-06-18";
3141
- async function handleMcp(req, res) {
3142
- const msg = req.body ?? {};
3143
- const reply = (result) => res.json({ jsonrpc: "2.0", id: msg.id, result });
3144
- switch (msg.method) {
3145
- case "initialize":
3146
- reply({
3147
- protocolVersion: typeof msg.params?.protocolVersion === "string" ? msg.params.protocolVersion : FALLBACK_PROTOCOL,
3148
- capabilities: { tools: {} },
3149
- serverInfo: { name: "planche", version: "1" }
3150
- });
3151
- return;
3152
- case "tools/list":
3153
- reply({
3154
- tools: bridgeTools().map((tool) => ({
3155
- name: tool.name,
3156
- description: tool.description,
3157
- inputSchema: tool.inputSchema
3158
- }))
3159
- });
3160
- return;
3161
- case "tools/call": {
3162
- const name = typeof msg.params?.name === "string" ? msg.params.name : "";
3163
- const args = msg.params?.arguments ?? {};
3164
- const { content, isError } = await callThroughBridge(name, args);
3165
- reply({ content: [{ type: "text", text: content }], isError });
3166
- return;
3167
- }
3168
- default:
3169
- if (msg.id === void 0) {
3170
- res.status(202).end();
3171
- return;
3172
- }
3173
- reply({});
82
+ var VERSION_SPEC = /^(latest|next|\d+\.\d+\.\d+(-[0-9A-Za-z.]+)?)$/;
83
+ function installedVersion(prefix) {
84
+ try {
85
+ const pkg = JSON.parse(readFileSync(join(prefix, "node_modules", "planche", "package.json"), "utf8"));
86
+ return pkg.version ?? null;
87
+ } catch {
88
+ return null;
3174
89
  }
3175
90
  }
3176
-
3177
- // src/tunneling.ts
3178
- import { randomUUID } from "crypto";
3179
-
3180
- // src/drivers/connection-lost.ts
3181
- var ConnectionLostError = class extends Error {
3182
- status = 404;
3183
- constructor(message) {
3184
- super(message);
3185
- this.name = "ConnectionLostError";
91
+ function runUpgrade(target) {
92
+ if (!VERSION_SPEC.test(target)) {
93
+ console.error(`Not a version: ${target}. Use \`planche upgrade\` or \`planche upgrade 1.2.3\`.`);
94
+ return 2;
3186
95
  }
3187
- };
3188
- var LOST_CODES = /* @__PURE__ */ new Set([
3189
- // mysql2
3190
- "PROTOCOL_CONNECTION_LOST",
3191
- "PROTOCOL_ENQUEUE_AFTER_FATAL_ERROR",
3192
- "PROTOCOL_ENQUEUE_AFTER_QUIT",
3193
- "PROTOCOL_ENQUEUE_AFTER_DESTROY",
3194
- // node sockets (pg, mssql/tedious surface these directly)
3195
- "ECONNRESET",
3196
- "EPIPE",
3197
- "ETIMEDOUT",
3198
- "ECONNABORTED",
3199
- "ENETUNREACH",
3200
- "EHOSTUNREACH",
3201
- "ENETDOWN",
3202
- // mssql
3203
- "ESOCKET",
3204
- "ECLOSE",
3205
- "ENOTOPEN",
3206
- // pg: admin_shutdown / crash_shutdown / cannot_connect_now
3207
- "57P01",
3208
- "57P02",
3209
- "57P03"
3210
- ]);
3211
- var LOST_MESSAGES = [
3212
- /connection is in closed state/i,
3213
- // mysql2 "Can't add new command when connection is in closed state"
3214
- /connection lost/i,
3215
- /connection terminated/i,
3216
- // pg
3217
- /client has encountered a connection error/i,
3218
- // pg
3219
- /client was closed and is not queryable/i,
3220
- // pg
3221
- /connection is closed/i,
3222
- // mssql
3223
- /socket hang up/i,
3224
- /ORA-0311[34]\b/,
3225
- /ORA-01012\b/,
3226
- /ORA-12541\b/,
3227
- /ORA-03135\b/,
3228
- /NJS-500\b/,
3229
- /NJS-521\b/
3230
- // oracle
3231
- ];
3232
- function isConnectionLost(err) {
3233
- if (!err || typeof err !== "object") return false;
3234
- const e = err;
3235
- if (typeof e.code === "string" && LOST_CODES.has(e.code)) return true;
3236
- if (e.fatal === true) return true;
3237
- if (typeof e.errorNum === "number" && [3113, 3114, 1012, 12541, 3135].includes(e.errorNum)) return true;
3238
- const message = typeof e.message === "string" ? e.message : "";
3239
- if (LOST_MESSAGES.some((re) => re.test(message))) return true;
3240
- return e.originalError !== void 0 && e.originalError !== err && isConnectionLost(e.originalError);
3241
- }
3242
-
3243
- // src/read-only.ts
3244
- var ReadOnlyViolation = class extends Error {
3245
- status = 403;
3246
- constructor(message) {
3247
- super(message);
3248
- this.name = "ReadOnlyViolation";
96
+ const { root, manifest } = locate();
97
+ if (!root || !manifest) {
98
+ console.error("This copy of planche was not installed by planche.dev/install.sh.");
99
+ console.error("Run the latest version with: npx planche@latest");
100
+ return 1;
3249
101
  }
3250
- };
3251
- var READ_KEYWORDS = /* @__PURE__ */ new Set(["SELECT", "WITH", "SHOW", "DESCRIBE", "DESC", "EXPLAIN", "VALUES", "TABLE", "PRAGMA"]);
3252
- function splitStatements(sql2) {
3253
- const out = [];
3254
- let cur = "";
3255
- let i = 0;
3256
- const n = sql2.length;
3257
- while (i < n) {
3258
- const ch = sql2[i];
3259
- const next = sql2[i + 1];
3260
- if (ch === "-" && next === "-") {
3261
- while (i < n && sql2[i] !== "\n") i++;
3262
- cur += " ";
3263
- continue;
3264
- }
3265
- if (ch === "#") {
3266
- while (i < n && sql2[i] !== "\n") i++;
3267
- cur += " ";
3268
- continue;
3269
- }
3270
- if (ch === "/" && next === "*") {
3271
- i += 2;
3272
- while (i < n && !(sql2[i] === "*" && sql2[i + 1] === "/")) i++;
3273
- i += 2;
3274
- cur += " ";
3275
- continue;
3276
- }
3277
- if (ch === "'" || ch === '"' || ch === "`" || ch === "[") {
3278
- const close = ch === "[" ? "]" : ch;
3279
- i++;
3280
- while (i < n) {
3281
- if (sql2[i] === "\\" && close === "'") {
3282
- i += 2;
3283
- continue;
3284
- }
3285
- if (sql2[i] === close) {
3286
- if (sql2[i + 1] === close && close !== "]") {
3287
- i += 2;
3288
- continue;
3289
- }
3290
- break;
3291
- }
3292
- i++;
3293
- }
3294
- i++;
3295
- cur += ch === "[" ? " x " : " 'x' ";
3296
- continue;
3297
- }
3298
- if (ch === "$" && next === "$") {
3299
- const end = sql2.indexOf("$$", i + 2);
3300
- i = end === -1 ? n : end + 2;
3301
- cur += " 'x' ";
3302
- continue;
3303
- }
3304
- if (ch === ";") {
3305
- out.push(cur);
3306
- cur = "";
3307
- i++;
3308
- continue;
3309
- }
3310
- cur += ch;
3311
- i++;
102
+ const npm = npmCommand(manifest);
103
+ if (!npm) {
104
+ console.error(`Cannot find npm for this install (see ${join(root, MANIFEST)}).`);
105
+ console.error("Re-run the installer: curl -fsSL https://planche.dev/install.sh | sh");
106
+ return 1;
3312
107
  }
3313
- out.push(cur);
3314
- return out.map((s) => s.trim()).filter((s) => s.length > 0);
3315
- }
3316
- function assertReadOnlyStatement(sql2) {
3317
- const statements = splitStatements(sql2);
3318
- if (statements.length === 0) return;
3319
- if (statements.length > 1) throw new ReadOnlyViolation("Read-only profile: one statement per request");
3320
- const stmt = statements[0];
3321
- const first = /^[A-Za-z]+/.exec(stmt)?.[0].toUpperCase() ?? "";
3322
- if (!READ_KEYWORDS.has(first)) throw new ReadOnlyViolation(`Read-only profile: ${first || "this statement"} is not allowed`);
3323
- if (first === "WITH") {
3324
- const words = stmt.toUpperCase().match(/\b(INSERT|UPDATE|DELETE|MERGE|REPLACE)\b/);
3325
- if (words) throw new ReadOnlyViolation(`Read-only profile: ${words[1]} inside WITH is not allowed`);
108
+ const prefix = join(root, "pkg");
109
+ const before = installedVersion(prefix);
110
+ console.log(`Installing planche@${target} into ${prefix} ...`);
111
+ const result = spawnSync(
112
+ npm.cmd,
113
+ [
114
+ ...npm.args,
115
+ "install",
116
+ "--prefix",
117
+ prefix,
118
+ "--omit=dev",
119
+ "--no-audit",
120
+ "--no-fund",
121
+ "--loglevel=error",
122
+ // Without this npm resolves `latest` from its cached registry document,
123
+ // which is exactly wrong in the minutes after a release: it reports "up
124
+ // to date", exits 0, and leaves the old version in place.
125
+ "--prefer-online",
126
+ `planche@${target}`
127
+ ],
128
+ { stdio: "inherit", shell: npm.shell, env: npmEnv(npm.node) }
129
+ );
130
+ if (result.status !== 0) {
131
+ console.error("Upgrade failed. Re-run the installer to repair the install:");
132
+ console.error(" curl -fsSL https://planche.dev/install.sh | sh");
133
+ return result.status ?? 1;
3326
134
  }
3327
- if (first === "PRAGMA" && /[=(]/.test(stmt)) throw new ReadOnlyViolation("Read-only profile: PRAGMA may only read");
3328
- if (/\bINTO\s+(OUTFILE|DUMPFILE)\b/i.test(stmt)) throw new ReadOnlyViolation("Read-only profile: INTO OUTFILE is not allowed");
3329
- if (/\bFOR\s+(UPDATE|SHARE|NO\s+KEY\s+UPDATE|KEY\s+SHARE)\b/i.test(stmt)) throw new ReadOnlyViolation("Read-only profile: locking reads are not allowed");
3330
- if (/\bLOCK\s+IN\s+SHARE\s+MODE\b/i.test(stmt)) throw new ReadOnlyViolation("Read-only profile: locking reads are not allowed");
3331
- if (first === "SELECT" && /\bINTO\b/i.test(stmt) && !/\bINTO\s+@/i.test(stmt)) throw new ReadOnlyViolation("Read-only profile: SELECT INTO is not allowed");
3332
- }
3333
-
3334
- // src/tunneling.ts
3335
- var IDLE_TIMEOUT_MS = Number(process.env.PLANCHE_IDLE_TIMEOUT_MS) || 30 * 60 * 1e3;
3336
- var MAX_CONNECTIONS = Number(process.env.PLANCHE_MAX_CONNECTIONS) || 64;
3337
- var ConnectionManager = class {
3338
- connections = /* @__PURE__ */ new Map();
3339
- lastUsed = /* @__PURE__ */ new Map();
3340
- /** Connections opened for a read-only gateway profile: writes are refused before reaching the driver. */
3341
- readOnly = /* @__PURE__ */ new Set();
3342
- constructor() {
3343
- const timer2 = setInterval(() => void this.closeIdle(), 6e4);
3344
- timer2.unref();
135
+ const installed = installedVersion(prefix) ?? target;
136
+ try {
137
+ writeFileSync(join(root, MANIFEST), `${JSON.stringify({ ...manifest, modules: process.versions.modules, installedAt: (/* @__PURE__ */ new Date()).toISOString() }, null, 2)}
138
+ `);
139
+ } catch {
3345
140
  }
3346
- /** Close connections idle for longer than `IDLE_TIMEOUT_MS`. */
3347
- async closeIdle() {
3348
- const cutoff = Date.now() - IDLE_TIMEOUT_MS;
3349
- for (const [id, used] of this.lastUsed) {
3350
- if (used < cutoff) {
3351
- console.log(`[ConnectionManager] Closing idle connection: ${id}`);
3352
- await this.disconnect(id).catch(() => void 0);
3353
- }
3354
- }
141
+ cached = null;
142
+ clearUpdateCache();
143
+ console.log(before === installed ? `planche ${installed} was already installed; nothing changed.` : `planche ${installed} installed. Start it with: planche`);
144
+ return 0;
145
+ }
146
+ async function runUninstall(assumeYes) {
147
+ const { root, manifest } = locate();
148
+ if (!root || !manifest) {
149
+ console.error("This copy of planche was not installed by planche.dev/install.sh; there is nothing to uninstall.");
150
+ console.error("An `npx planche` download lives in the npm cache and is cleared with: npm cache clean --force");
151
+ return 1;
3355
152
  }
3356
- /**
3357
- * Open a new connection and return a unique connection ID.
3358
- */
3359
- async connect(config, options = {}) {
3360
- if (this.connections.size >= MAX_CONNECTIONS) {
3361
- throw new Error(`Too many open connections (${MAX_CONNECTIONS}); disconnect some first`);
153
+ const targets = [root, ...manifest.bin && existsSync(manifest.bin) ? [manifest.bin] : []];
154
+ console.log("This will remove:");
155
+ for (const t of targets) console.log(` ${t}`);
156
+ console.log(`
157
+ Your settings stay: ${PLANCHE_DIR} (known_hosts, usage counts) is left untouched.`);
158
+ if (!assumeYes) {
159
+ if (!process.stdin.isTTY) {
160
+ console.error("\nRefusing to delete without a confirmation. Re-run with: planche uninstall --yes");
161
+ return 1;
3362
162
  }
3363
- const driver = await openDriver(config);
3364
- if (options.readOnly && driver.setReadOnly) {
3365
- try {
3366
- await driver.setReadOnly();
3367
- } catch (err) {
3368
- await driver.close().catch(() => void 0);
3369
- throw err;
3370
- }
163
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
164
+ const answer = (await rl.question("\nRemove them? [y/N] ")).trim().toLowerCase();
165
+ rl.close();
166
+ if (answer !== "y" && answer !== "yes") {
167
+ console.log("Nothing was removed.");
168
+ return 1;
3371
169
  }
3372
- const connectionId = randomUUID();
3373
- this.connections.set(connectionId, driver);
3374
- this.lastUsed.set(connectionId, Date.now());
3375
- if (options.readOnly) this.readOnly.add(connectionId);
3376
- console.log(`[ConnectionManager] New ${driver.type} connection: ${connectionId} -> ${driver.label}`);
3377
- return connectionId;
3378
170
  }
3379
- has(connectionId) {
3380
- return this.connections.has(connectionId);
3381
- }
3382
- /**
3383
- * Execute a SQL query on an existing connection.
3384
- * Optionally switch database before executing (drivers that support it).
3385
- */
3386
- async query(connectionId, sql2, database) {
3387
- const driver = this.connections.get(connectionId);
3388
- if (!driver) {
3389
- throw new Error(`Connection not found: ${connectionId}`);
3390
- }
3391
- this.lastUsed.set(connectionId, Date.now());
3392
- if (this.readOnly.has(connectionId)) assertReadOnlyStatement(sql2);
171
+ for (const t of targets) {
3393
172
  try {
3394
- return await driver.query(sql2, database);
173
+ rmSync(t, { recursive: true, force: true });
3395
174
  } catch (err) {
3396
- if (!isConnectionLost(err)) throw err;
3397
- console.log(`[ConnectionManager] Connection lost, evicting: ${connectionId}`);
3398
- this.connections.delete(connectionId);
3399
- this.lastUsed.delete(connectionId);
3400
- this.readOnly.delete(connectionId);
3401
- await driver.close().catch(() => void 0);
3402
- throw new ConnectionLostError(`Connection lost: ${err instanceof Error ? err.message : String(err)}`);
3403
- }
3404
- }
3405
- /**
3406
- * Interrupt whatever is running on a connection. Best effort: an engine with
3407
- * no way to interrupt (better-sqlite3 blocks the thread) reports false rather
3408
- * than claiming a cancel that did not happen. The connection stays open —
3409
- * the point is to free it, not to drop it.
3410
- */
3411
- async cancel(connectionId) {
3412
- const driver = this.connections.get(connectionId);
3413
- if (!driver) throw new Error(`Connection not found: ${connectionId}`);
3414
- if (!driver.cancel) return { cancelled: false, abandoned: false };
3415
- await driver.cancel();
3416
- return { cancelled: true, abandoned: driver.cancelKind === "abandon" };
3417
- }
3418
- /**
3419
- * Close and remove a connection.
3420
- */
3421
- async disconnect(connectionId) {
3422
- const driver = this.connections.get(connectionId);
3423
- if (!driver) {
3424
- throw new Error(`Connection not found: ${connectionId}`);
175
+ console.error(`Could not remove ${t}: ${err instanceof Error ? err.message : String(err)}`);
176
+ return 1;
3425
177
  }
3426
- await driver.close();
3427
- this.connections.delete(connectionId);
3428
- this.lastUsed.delete(connectionId);
3429
- this.readOnly.delete(connectionId);
3430
- console.log(`[ConnectionManager] Disconnected: ${connectionId}`);
3431
- }
3432
- /**
3433
- * Validate a config by opening and immediately closing a connection.
3434
- */
3435
- async testConnection(config) {
3436
- await testDriver(config);
3437
- return true;
3438
178
  }
3439
- /**
3440
- * Get the number of active connections.
3441
- */
3442
- get activeConnections() {
3443
- return this.connections.size;
3444
- }
3445
- /**
3446
- * Close all connections (for graceful shutdown).
3447
- */
3448
- async disconnectAll() {
3449
- const promises = Array.from(this.connections.entries()).map(
3450
- async ([id, driver]) => {
3451
- try {
3452
- await driver.close();
3453
- console.log(`[ConnectionManager] Disconnected: ${id}`);
3454
- } catch {
3455
- }
3456
- }
3457
- );
3458
- await Promise.all(promises);
3459
- this.connections.clear();
3460
- this.lastUsed.clear();
3461
- this.readOnly.clear();
3462
- }
3463
- };
3464
-
3465
- // src/server.ts
3466
- import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
3467
- import { join as join10 } from "path";
3468
- function errorStatus(err) {
3469
- const status = err?.status;
3470
- return typeof status === "number" && status >= 400 && status < 600 ? status : 400;
3471
- }
3472
- function fail(res, status, message, extra = {}) {
3473
- res.status(status).json({ success: false, message, ...extra });
179
+ console.log("planche has been uninstalled.");
180
+ return 0;
3474
181
  }
3475
- function errorExtra(err) {
3476
- if (err instanceof HostKeyError) {
3477
- const body = { code: "host-key-unknown", hostKey: err.hostKey };
3478
- return body;
182
+ function readUpdateCache() {
183
+ try {
184
+ const data = JSON.parse(readFileSync(UPDATE_CACHE, "utf8"));
185
+ return typeof data.checkedAt === "number" && typeof data.latest === "string" ? data : null;
186
+ } catch {
187
+ return null;
3479
188
  }
3480
- return {};
3481
189
  }
3482
- var errorMessage = (err) => err instanceof Error ? err.message : String(err);
3483
- function secretEquals(given, expected) {
3484
- const a = createHash2("sha256").update(given).digest();
3485
- const b = createHash2("sha256").update(expected).digest();
3486
- return timingSafeEqual2(a, b);
3487
- }
3488
- function bearerOf(header) {
3489
- return header?.startsWith("Bearer ") ? header.slice(7) : null;
3490
- }
3491
- var AuthThrottle = class {
3492
- constructor(max = 10, windowMs = 6e4, lockMs = 3e4) {
3493
- this.max = max;
3494
- this.windowMs = windowMs;
3495
- this.lockMs = lockMs;
3496
- }
3497
- max;
3498
- windowMs;
3499
- lockMs;
3500
- failures = /* @__PURE__ */ new Map();
3501
- lockedUntil = /* @__PURE__ */ new Map();
3502
- /** Keyed by peer address so one local process cannot lock the browser out. */
3503
- locked(peer) {
3504
- return Date.now() < (this.lockedUntil.get(peer) ?? 0);
3505
- }
3506
- fail(peer) {
3507
- const now = Date.now();
3508
- const recent = (this.failures.get(peer) ?? []).filter((t) => now - t < this.windowMs);
3509
- recent.push(now);
3510
- if (recent.length >= this.max) {
3511
- this.lockedUntil.set(peer, now + this.lockMs);
3512
- this.failures.delete(peer);
3513
- console.warn(`[agent] Too many bad tokens from ${peer}; refusing its authentication for ${this.lockMs / 1e3}s`);
3514
- } else {
3515
- this.failures.set(peer, recent);
3516
- }
3517
- if (this.failures.size > 1e3) this.failures.clear();
190
+ function writeUpdateCache(latest) {
191
+ try {
192
+ mkdirSync(PLANCHE_DIR, { recursive: true, mode: 448 });
193
+ writeFileSync(UPDATE_CACHE, `${JSON.stringify({ checkedAt: Date.now(), latest })}
194
+ `, { mode: 384 });
195
+ } catch {
3518
196
  }
3519
- };
3520
- function accessControl(allowed) {
3521
- return (req, res, next) => {
3522
- const origin = req.headers.origin;
3523
- if (origin === void 0) {
3524
- next();
3525
- return;
3526
- }
3527
- if (!allowed.has(origin)) {
3528
- fail(res, 403, `Origin not allowed: ${origin}`);
3529
- return;
3530
- }
3531
- res.setHeader("Access-Control-Allow-Origin", origin);
3532
- res.setHeader("Vary", "Origin");
3533
- res.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
3534
- res.setHeader("Access-Control-Allow-Headers", "Authorization, Content-Type");
3535
- res.setHeader("Access-Control-Max-Age", "600");
3536
- if (req.headers["access-control-request-private-network"] === "true") {
3537
- res.setHeader("Access-Control-Allow-Private-Network", "true");
3538
- }
3539
- if (req.headers["access-control-request-local-network"] === "true") {
3540
- res.setHeader("Access-Control-Allow-Local-Network", "true");
3541
- }
3542
- if (req.method === "OPTIONS") {
3543
- res.sendStatus(204);
3544
- return;
3545
- }
3546
- next();
3547
- };
3548
197
  }
3549
- function createAgent(options) {
3550
- const app = express();
3551
- const manager = new ConnectionManager();
3552
- const allowedOrigins = new Set(options.allowedOrigins.map((o) => o.replace(/\/+$/, "")));
3553
- const allowedHosts = options.allowedHosts ? new Set(options.allowedHosts.map((h) => h.toLowerCase())) : null;
3554
- const throttle = new AuthThrottle();
3555
- const pairThrottle = new AuthThrottle();
3556
- let pairing = options.pairingCode;
3557
- let lastPairOffer = 0;
3558
- app.set("env", "production");
3559
- app.disable("x-powered-by");
3560
- app.disable("etag");
3561
- const peerOf = (req) => req.socket.remoteAddress ?? "unknown";
3562
- const authorized = (req) => {
3563
- const peer = peerOf(req);
3564
- if (throttle.locked(peer)) return false;
3565
- const given = bearerOf(req.headers.authorization);
3566
- if (given === null) return false;
3567
- const ok = secretEquals(given, options.authToken);
3568
- if (!ok) throttle.fail(peer);
3569
- return ok;
3570
- };
3571
- if (options.logRequests) {
3572
- app.use((req, _res, next) => {
3573
- const hasAuth = !!req.headers.authorization;
3574
- console.log(`[${(/* @__PURE__ */ new Date()).toISOString()}] ${req.method} ${req.url} (auth: ${hasAuth})`);
3575
- next();
3576
- });
3577
- }
3578
- app.use((req, res, next) => {
3579
- res.setHeader("Cache-Control", "no-store");
3580
- res.setHeader("X-Content-Type-Options", "nosniff");
3581
- if (allowedHosts) {
3582
- const host = req.headers.host?.toLowerCase();
3583
- if (!host || !allowedHosts.has(host)) {
3584
- fail(res, 403, "Invalid Host header");
3585
- return;
3586
- }
3587
- }
3588
- next();
3589
- });
3590
- app.use(accessControl(allowedOrigins));
3591
- if (options.appDir) serveApp(app, options.appDir);
3592
- const PAIR_OFFER_INTERVAL_MS = 6e4;
3593
- function offerPairing(req) {
3594
- if (!options.onUnpaired) return false;
3595
- const origin = req.headers.origin;
3596
- if (typeof origin !== "string" || !allowedOrigins.has(origin.replace(/\/+$/, ""))) return false;
3597
- const now = Date.now();
3598
- if (pairing && now <= pairing.expiresAt) return false;
3599
- if (now - lastPairOffer < PAIR_OFFER_INTERVAL_MS) return false;
3600
- lastPairOffer = now;
3601
- pairing = { code: randomUUID2(), expiresAt: now + 10 * 60 * 1e3 };
3602
- options.onUnpaired(pairing, origin);
3603
- return true;
198
+ function clearUpdateCache() {
199
+ try {
200
+ rmSync(UPDATE_CACHE, { force: true });
201
+ } catch {
3604
202
  }
3605
- app.get("/health", async (req, res) => {
3606
- const body = {
3607
- success: true,
3608
- name: "planche",
3609
- version: options.version,
3610
- protocolVersion: PROTOCOL_VERSION,
3611
- activeConnections: manager.activeConnections,
3612
- uptime: process.uptime()
3613
- };
3614
- const ai = await aiConfigView();
3615
- body.ai = { configured: ai.configured, model: ai.model, tools: true };
3616
- if (options.install) body.install = options.install;
3617
- if (options.gateway) body.gateway = { name: options.gateway.name, profiles: options.gateway.profiles.length };
3618
- if (req.headers.authorization !== void 0) {
3619
- body.authorized = authorized(req);
3620
- }
3621
- if (body.authorized !== true) void offerPairing(req);
3622
- res.json(body);
3623
- });
3624
- app.post("/pair", express.json({ limit: "1kb" }), (req, res) => {
3625
- if (pairThrottle.locked(peerOf(req))) {
3626
- fail(res, 429, "Too many failed pairing attempts; try again shortly");
3627
- return;
3628
- }
3629
- const code = typeof req.body?.code === "string" ? req.body.code : "";
3630
- if (!pairing || Date.now() > pairing.expiresAt || !code || !secretEquals(code, pairing.code)) {
3631
- pairThrottle.fail(peerOf(req));
3632
- const printed = offerPairing(req);
3633
- 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");
3634
- return;
3635
- }
3636
- pairing = void 0;
3637
- const body = { success: true, token: options.authToken };
3638
- res.json(body);
3639
- });
3640
- async function mcpAuthorized(req) {
3641
- if (req.path !== "/mcp") return false;
3642
- const given = bearerOf(req.headers.authorization);
3643
- const expected = await readMcpToken();
3644
- if (!given || !expected) return false;
3645
- return secretEquals(given, expected);
203
+ }
204
+ function isNewer(candidate, current) {
205
+ const parse = (v) => /^\d+\.\d+\.\d+$/.test(v) ? v.split(".").map(Number) : null;
206
+ const a = parse(candidate);
207
+ const b = parse(current);
208
+ if (!a || !b) return false;
209
+ for (let i = 0; i < 3; i += 1) {
210
+ if (a[i] !== b[i]) return a[i] > b[i];
3646
211
  }
3647
- app.use((req, res, next) => {
3648
- if (throttle.locked(peerOf(req))) {
3649
- fail(res, 429, "Too many failed authentication attempts; try again shortly");
3650
- return;
3651
- }
3652
- if (authorized(req)) {
3653
- next();
3654
- return;
3655
- }
3656
- void mcpAuthorized(req).then((ok) => {
3657
- if (ok) {
3658
- next();
3659
- return;
3660
- }
3661
- fail(res, 401, "Unauthorized: invalid or missing auth token");
3662
- });
3663
- });
3664
- app.use(express.json({ limit: options.bodyLimit }));
3665
- app.get("/profiles", (_req, res) => {
3666
- if (!options.gateway) {
3667
- fail(res, 404, "Not a gateway");
3668
- return;
3669
- }
3670
- const body = { success: true, gateway: options.gateway.name, profiles: options.gateway.profiles.map(describeProfile) };
3671
- res.json(body);
3672
- });
3673
- const resolveConnect = (body) => {
3674
- const b = body ?? {};
3675
- if (options.gateway) {
3676
- if (typeof b.profile !== "string") throw Object.assign(new Error("Gateway mode: choose a profile (POST { profile })"), { status: 403 });
3677
- const entry = options.gateway.profiles.find((p) => p.name === b.profile);
3678
- if (!entry) throw Object.assign(new Error(`Unknown gateway profile: ${b.profile}`), { status: 404 });
3679
- return { config: entry.config, readOnly: entry.readOnly };
3680
- }
3681
- return { config: parseDriverConfig(b), readOnly: false };
3682
- };
3683
- app.post("/connect", async (req, res) => {
3684
- try {
3685
- const { config, readOnly } = resolveConnect(req.body);
3686
- const connectionId = await manager.connect(config, { readOnly });
3687
- res.json({ success: true, connectionId, message: "Connection Successful" });
3688
- } catch (err) {
3689
- const message = errorMessage(err);
3690
- console.error("[connect] Error:", message);
3691
- fail(res, errorStatus(err), message, errorExtra(err));
3692
- }
3693
- });
3694
- app.post("/query", async (req, res) => {
3695
- try {
3696
- const { connectionId, sql: sql2, database } = req.body ?? {};
3697
- if (typeof connectionId !== "string" || typeof sql2 !== "string" || !connectionId || !sql2) {
3698
- fail(res, 400, "connectionId and sql are required");
3699
- return;
3700
- }
3701
- if (!manager.has(connectionId)) {
3702
- fail(res, 404, `Connection not found: ${connectionId}`);
3703
- return;
3704
- }
3705
- const start = Date.now();
3706
- const result = await manager.query(connectionId, sql2, typeof database === "string" ? database : void 0);
3707
- const execTime = Date.now() - start;
3708
- res.json({
3709
- success: true,
3710
- fields: result.fields,
3711
- records: result.records,
3712
- affectedRows: result.affectedRows,
3713
- insertId: result.insertId,
3714
- isResultQuery: result.isResultQuery,
3715
- execTime
3716
- });
3717
- } catch (err) {
3718
- const message = errorMessage(err);
3719
- console.error("[query] Error:", message);
3720
- if (err instanceof ConnectionLostError) {
3721
- fail(res, 404, message);
3722
- return;
3723
- }
3724
- fail(res, 422, message);
3725
- }
3726
- });
3727
- app.post("/usage", (req, res) => {
3728
- const feature = (req.body ?? {}).feature;
3729
- if (isProFeature(feature)) recordUsage(feature);
3730
- res.json({ success: true });
3731
- });
3732
- app.get("/usage", async (_req, res) => {
3733
- await flushUsage();
3734
- res.json(await usageSnapshot());
3735
- });
3736
- app.get("/ai/config", async (_req, res) => {
3737
- res.json({ success: true, ...await aiConfigView() });
3738
- });
3739
- app.put("/ai/config", async (req, res) => {
3740
- const current = await aiConfigView();
3741
- if (current.fromEnvironment) {
3742
- fail(res, 409, "This agent's AI settings come from its environment and cannot be changed from the app.");
3743
- return;
3744
- }
3745
- const body = req.body ?? {};
3746
- const provider = body.provider === "openai" || body.provider === "gemini" || body.provider === "cli" || body.provider === "anthropic-profile" ? body.provider : "anthropic";
3747
- const keyless = isKeyless(provider);
3748
- const key = typeof body.key === "string" ? body.key.trim() : "";
3749
- if (!key && !keyless) {
3750
- fail(res, 400, "key is required");
3751
- return;
3752
- }
3753
- const model = typeof body.model === "string" && body.model.trim() ? body.model.trim() : defaultModelFor(provider);
3754
- if (provider === "cli" && !isCliCommand(model)) {
3755
- fail(res, 400, `model must name an installed CLI: ${CLI_COMMANDS.join(", ")}`);
3756
- return;
3757
- }
3758
- const baseUrl = typeof body.baseUrl === "string" ? body.baseUrl.trim() : "";
3759
- if (baseUrl && !/^https?:\/\//i.test(baseUrl)) {
3760
- fail(res, 400, "baseUrl must be an http(s) URL");
3761
- return;
3762
- }
3763
- const written = await saveAiConfig({
3764
- provider,
3765
- model,
3766
- baseUrl: baseUrl || void 0,
3767
- key
3768
- }, body.remember === true);
3769
- console.log(
3770
- provider === "cli" ? `[agent] AI answers will come from the local "${model}" CLI; no key is held. The choice is kept in ${aiConfigPath}.` : provider === "anthropic-profile" ? `[agent] AI answers will use this machine's own Anthropic credential; no key is held. The choice is kept in ${aiConfigPath}.` : written ? `[agent] AI key saved to ${aiConfigPath} (owner-only). Remove it with "Forget key" in Settings, or by deleting that file.` : "[agent] AI key accepted for this session only; it is not written to disk."
3771
- );
3772
- res.json({ success: true, ...await aiConfigView() });
3773
- });
3774
- app.delete("/ai/config", async (_req, res) => {
3775
- await forgetAiConfig();
3776
- res.json({ success: true, ...await aiConfigView() });
3777
- });
3778
- app.post("/ai/run", async (req, res) => {
3779
- const body = req.body ?? {};
3780
- const hasTurns = Array.isArray(body.messages) && body.messages.length > 0;
3781
- const hasUser = typeof body.user === "string" && body.user.trim() !== "";
3782
- if (typeof body.system !== "string" || !hasTurns && !hasUser) {
3783
- fail(res, 400, "system and one of user/messages are required");
3784
- return;
3785
- }
3786
- const stop = new AbortController();
3787
- res.on("close", () => {
3788
- if (!res.writableFinished) stop.abort();
3789
- });
3790
- try {
3791
- const answer = await runPrompt({
3792
- system: body.system,
3793
- ...hasUser ? { user: body.user } : {},
3794
- ...hasTurns ? { messages: body.messages } : {},
3795
- ...Array.isArray(body.tools) && body.tools.length > 0 ? { tools: body.tools } : {},
3796
- model: typeof body.model === "string" ? body.model : void 0
3797
- }, stop.signal);
3798
- if (!stop.signal.aborted) res.json(answer);
3799
- } catch (err) {
3800
- if (stop.signal.aborted) return;
3801
- const e = err instanceof AiError ? err : new AiError(502, String(err));
3802
- fail(res, e.status, e.message);
3803
- }
3804
- });
3805
- app.post("/mcp", async (req, res) => {
3806
- await handleMcp(req, res);
3807
- });
3808
- app.get("/bridge/events", (req, res) => {
3809
- res.writeHead(200, {
3810
- "content-type": "text/event-stream",
3811
- "cache-control": "no-store",
3812
- connection: "keep-alive"
3813
- });
3814
- res.write(": open\n\n");
3815
- attachBridge(res);
3816
- });
3817
- app.post("/bridge/tools", (req, res) => {
3818
- const tools2 = req.body?.tools;
3819
- if (!Array.isArray(tools2)) {
3820
- fail(res, 400, "tools must be an array");
3821
- return;
3822
- }
3823
- setBridgeTools(tools2);
3824
- res.json({ success: true });
3825
- });
3826
- app.post("/bridge/result", (req, res) => {
3827
- const { id, content, isError } = req.body ?? {};
3828
- if (typeof id !== "string") {
3829
- fail(res, 400, "id is required");
3830
- return;
3831
- }
3832
- resolveBridgeCall(id, typeof content === "string" ? content : "", isError === true);
3833
- res.json({ success: true });
3834
- });
3835
- app.post("/mcp/setup", async (_req, res) => {
3836
- const url = mcpUrl();
3837
- if (!url) {
3838
- fail(res, 503, "The agent is not listening yet.");
3839
- return;
3840
- }
3841
- const body = { success: true, url, token: await ensureMcpToken(), path: mcpTokenPath };
3842
- console.log(`[agent] MCP token handed to the app; stored at ${mcpTokenPath} (owner-only).`);
3843
- res.json(body);
3844
- });
3845
- app.delete("/mcp/setup", async (_req, res) => {
3846
- await forgetMcpToken();
3847
- console.log(`[agent] MCP token revoked; ${mcpTokenPath} removed.`);
3848
- res.json({ success: true });
3849
- });
3850
- app.post("/cancel", async (req, res) => {
3851
- try {
3852
- const connectionId = req.body?.connectionId;
3853
- if (typeof connectionId !== "string" || !connectionId) {
3854
- fail(res, 400, "connectionId is required");
3855
- return;
3856
- }
3857
- const { cancelled, abandoned } = await manager.cancel(connectionId);
3858
- const body = {
3859
- success: true,
3860
- cancelled,
3861
- abandoned,
3862
- message: !cancelled ? "This engine cannot interrupt a running statement" : abandoned ? "Stopped waiting for the request; the query may still finish upstream" : "Cancel sent"
3863
- };
3864
- res.json(body);
3865
- } catch (err) {
3866
- fail(res, errorStatus(err), errorMessage(err));
3867
- }
3868
- });
3869
- app.post("/disconnect", async (req, res) => {
3870
- try {
3871
- const { connectionId } = req.body ?? {};
3872
- if (typeof connectionId !== "string" || !connectionId) {
3873
- fail(res, 400, "connectionId is required");
3874
- return;
3875
- }
3876
- if (!manager.has(connectionId)) {
3877
- fail(res, 404, `Connection not found: ${connectionId}`);
3878
- return;
3879
- }
3880
- await manager.disconnect(connectionId);
3881
- res.json({ success: true, message: "Disconnected" });
3882
- } catch (err) {
3883
- const message = errorMessage(err);
3884
- console.error("[disconnect] Error:", message);
3885
- fail(res, 500, message);
3886
- }
3887
- });
3888
- app.post("/test-connection", async (req, res) => {
3889
- try {
3890
- await manager.testConnection(resolveConnect(req.body).config);
3891
- res.json({ success: true, message: "Connection test successful" });
3892
- } catch (err) {
3893
- const message = errorMessage(err);
3894
- console.error("[test-connection] Error:", message);
3895
- fail(res, errorStatus(err), message, errorExtra(err));
3896
- }
3897
- });
3898
- app.use((_req, res) => fail(res, 404, "Not found"));
3899
- app.use((err, _req, res, _next) => {
3900
- const e = err;
3901
- const status = e?.type === "entity.too.large" ? 413 : typeof e?.status === "number" && e.status >= 400 && e.status < 600 ? e.status : 400;
3902
- console.error("[agent] Request error:", e?.message ?? String(err));
3903
- if (res.headersSent) return;
3904
- fail(res, status, status === 413 ? "Request body too large" : status === 400 ? "Malformed request" : "Request failed");
3905
- });
3906
- return { app, manager };
212
+ return false;
3907
213
  }
3908
- function serveApp(app, appDir) {
3909
- const csp = [
3910
- "default-src 'self'",
3911
- "script-src 'self' 'wasm-unsafe-eval'",
3912
- "connect-src 'self'",
3913
- "img-src 'self' data: blob:",
3914
- "style-src 'self' 'unsafe-inline'",
3915
- "font-src 'self' data:",
3916
- "worker-src 'self' blob:",
3917
- "child-src 'self' blob:",
3918
- "object-src 'none'",
3919
- "base-uri 'none'",
3920
- "form-action 'none'",
3921
- "frame-ancestors 'none'"
3922
- ].join("; ");
3923
- const indexFile = join10(appDir, "index.html");
3924
- const indexHtml = existsSync3(indexFile) ? readFileSync3(indexFile, "utf8").replace("<head>", '<head>\n <meta name="planche-agent" content="same-origin" />') : null;
3925
- const headers = (res) => {
3926
- res.setHeader("Content-Security-Policy", csp);
3927
- res.setHeader("X-Frame-Options", "DENY");
3928
- res.setHeader("Referrer-Policy", "no-referrer");
3929
- res.setHeader("Cross-Origin-Opener-Policy", "same-origin");
3930
- };
3931
- app.get(["/", "/index.html"], (_req, res) => {
3932
- if (!indexHtml) {
3933
- fail(res, 404, "App not bundled");
3934
- return;
3935
- }
3936
- headers(res);
3937
- res.type("html").send(indexHtml);
3938
- });
3939
- app.use(express.static(appDir, {
3940
- index: false,
3941
- setHeaders: (res, path) => {
3942
- if (path.endsWith(".html")) headers(res);
3943
- if (/\/assets\//.test(path)) res.setHeader("Cache-Control", "public, max-age=31536000, immutable");
214
+ async function updateNotice(current) {
215
+ if (installKind() !== "standalone") return null;
216
+ if (process.env.PLANCHE_NO_UPDATE_CHECK) return null;
217
+ const cache = readUpdateCache();
218
+ let latest = cache && Date.now() - cache.checkedAt < CHECK_INTERVAL_MS ? cache.latest : null;
219
+ if (!latest) {
220
+ const controller = new AbortController();
221
+ const timer = setTimeout(() => controller.abort(), 2500);
222
+ try {
223
+ const response = await fetch(REGISTRY, { signal: controller.signal, headers: { accept: "application/json" } });
224
+ if (!response.ok) return null;
225
+ const data = await response.json();
226
+ if (typeof data.version !== "string" || !/^[\w.+-]{1,32}$/.test(data.version)) return null;
227
+ latest = data.version;
228
+ writeUpdateCache(latest);
229
+ } catch {
230
+ return null;
231
+ } finally {
232
+ clearTimeout(timer);
3944
233
  }
3945
- }));
234
+ }
235
+ return isNewer(latest, current) ? `planche ${latest} is available (you have ${current}) \u2014 update with: planche upgrade` : null;
3946
236
  }
3947
237
 
3948
238
  // src/index.ts
3949
- var { version: VERSION } = createRequire2(import.meta.url)("../package.json");
239
+ var { version: VERSION } = createRequire(import.meta.url)("../package.json");
3950
240
  var DEFAULT_APP_URL = "https://planche.dev";
3951
241
  var DEV_ORIGINS = [
3952
242
  "http://localhost:5173",
@@ -3956,7 +246,7 @@ var DEV_ORIGINS = [
3956
246
  ];
3957
247
  function bundledAppDir() {
3958
248
  const dir = fileURLToPath2(new URL("../app/", import.meta.url));
3959
- return existsSync4(join11(dir, "index.html")) ? dir : null;
249
+ return existsSync2(join2(dir, "index.html")) ? dir : null;
3960
250
  }
3961
251
  function isLoopbackUrl(url) {
3962
252
  try {
@@ -3967,19 +257,19 @@ function isLoopbackUrl(url) {
3967
257
  }
3968
258
  async function appIsReachable(url, timeoutMs = 2500) {
3969
259
  const controller = new AbortController();
3970
- const timer2 = setTimeout(() => controller.abort(), timeoutMs);
260
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
3971
261
  try {
3972
262
  const response = await fetch(url, { signal: controller.signal, redirect: "follow", headers: { "cache-control": "no-store" } });
3973
263
  return response.ok;
3974
264
  } catch {
3975
265
  return false;
3976
266
  } finally {
3977
- clearTimeout(timer2);
267
+ clearTimeout(timer);
3978
268
  }
3979
269
  }
3980
270
  function resolveAppDir(explicit) {
3981
- const dir = explicit ? resolve4(explicit) : fileURLToPath2(new URL("../app/", import.meta.url));
3982
- if (!existsSync4(join11(dir, "index.html"))) {
271
+ const dir = explicit ? resolve2(explicit) : fileURLToPath2(new URL("../app/", import.meta.url));
272
+ if (!existsSync2(join2(dir, "index.html"))) {
3983
273
  console.error(explicit ? `--app-dir has no index.html: ${dir}` : `This build does not include the web app (${dir}). Install the published package, or pass --app-dir <dir>.`);
3984
274
  process.exit(2);
3985
275
  }
@@ -4034,7 +324,7 @@ function openInBrowser(url) {
4034
324
  try {
4035
325
  const env = { ...process.env };
4036
326
  delete env.PLANCHE_AUTH_TOKEN;
4037
- spawn2(cmd, args, { stdio: "ignore", detached: true, env }).on("error", () => void 0).unref();
327
+ spawn(cmd, args, { stdio: "ignore", detached: true, env }).on("error", () => void 0).unref();
4038
328
  } catch {
4039
329
  }
4040
330
  }
@@ -4123,7 +413,7 @@ async function main() {
4123
413
  const port = Number(flags.port ?? process.env.PLANCHE_PORT) || 8888;
4124
414
  const addr = flags.addr ?? process.env.PLANCHE_ADDR ?? "127.0.0.1";
4125
415
  const tokenFromEnv = !flags.token && !!process.env.PLANCHE_AUTH_TOKEN;
4126
- const authToken = flags.token ?? process.env.PLANCHE_AUTH_TOKEN ?? randomUUID3();
416
+ const authToken = flags.token ?? process.env.PLANCHE_AUTH_TOKEN ?? randomUUID();
4127
417
  const gatewayFile = flags.gateway ?? process.env.PLANCHE_GATEWAY;
4128
418
  let gateway;
4129
419
  if (gatewayFile) {
@@ -4135,12 +425,12 @@ async function main() {
4135
425
  }
4136
426
  }
4137
427
  const hostedAppUrl = (flags.app ?? process.env.PLANCHE_APP_URL ?? DEFAULT_APP_URL).replace(/\/+$/, "");
4138
- let serveApp2 = flags["serve-app"] ?? Boolean(gateway);
4139
- const offlineFallback = !serveApp2 && flags["serve-app"] !== false && bundledAppDir() !== null && !isLoopbackUrl(hostedAppUrl) && !await appIsReachable(hostedAppUrl);
4140
- if (offlineFallback) serveApp2 = true;
4141
- const appDir = serveApp2 ? resolveAppDir(flags["app-dir"] ?? process.env.PLANCHE_APP_DIR) : void 0;
428
+ let serveApp = flags["serve-app"] ?? Boolean(gateway);
429
+ const offlineFallback = !serveApp && flags["serve-app"] !== false && bundledAppDir() !== null && !isLoopbackUrl(hostedAppUrl) && !await appIsReachable(hostedAppUrl);
430
+ if (offlineFallback) serveApp = true;
431
+ const appDir = serveApp ? resolveAppDir(flags["app-dir"] ?? process.env.PLANCHE_APP_DIR) : void 0;
4142
432
  const publicUrl = (flags["public-url"] ?? process.env.PLANCHE_PUBLIC_URL ?? "").replace(/\/+$/, "");
4143
- const appUrl = (serveApp2 ? publicUrl || null : null) ?? hostedAppUrl;
433
+ const appUrl = (serveApp ? publicUrl || null : null) ?? hostedAppUrl;
4144
434
  const shouldOpen = flags.open ?? true;
4145
435
  const bodyLimit = process.env.PLANCHE_BODY_LIMIT || "50mb";
4146
436
  const appOrigin = originOf(appUrl);
@@ -4173,12 +463,12 @@ async function main() {
4173
463
  }
4174
464
  const devApp = LOOPBACK_ADDRS.has(new URL(appUrl).hostname);
4175
465
  const allowedOrigins = [appOrigin, ...devApp ? DEV_ORIGINS : [], ...extraOrigins];
4176
- if (serveApp2) allowedOrigins.push(`http://127.0.0.1:${port}`, `http://localhost:${port}`, `http://[::1]:${port}`);
4177
- const pairingCode = { code: randomUUID3(), expiresAt: Date.now() + 10 * 60 * 1e3 };
466
+ if (serveApp) allowedOrigins.push(`http://127.0.0.1:${port}`, `http://localhost:${port}`, `http://[::1]:${port}`);
467
+ const pairingCode = { code: randomUUID(), expiresAt: Date.now() + 10 * 60 * 1e3 };
4178
468
  const listenAddr = addr.replace(/^\[(.*)\]$/, "$1");
4179
469
  const agentUrl = `http://${listenAddr.includes(":") ? `[${listenAddr}]` : listenAddr}:${port}`;
4180
470
  const divider = "-".repeat(72);
4181
- const pairLink = (code, appBase) => serveApp2 ? `${appBase}/#pair=${encodeURIComponent(code)}` : `${appBase}/#agent=${encodeURIComponent(agentUrl)}&pair=${encodeURIComponent(code)}`;
471
+ const pairLink = (code, appBase) => serveApp ? `${appBase}/#pair=${encodeURIComponent(code)}` : `${appBase}/#agent=${encodeURIComponent(agentUrl)}&pair=${encodeURIComponent(code)}`;
4182
472
  const { app, manager } = createAgent({
4183
473
  authToken,
4184
474
  allowedOrigins,
@@ -4196,7 +486,7 @@ async function main() {
4196
486
  // origin the request came from, so it points at the app that is really
4197
487
  // running — a dev server bumped to another port included.
4198
488
  onUnpaired: (fresh, appOrigin2) => {
4199
- const url = pairLink(fresh.code, serveApp2 ? publicUrl || appOrigin2 : appOrigin2);
489
+ const url = pairLink(fresh.code, serveApp ? publicUrl || appOrigin2 : appOrigin2);
4200
490
  console.log(divider);
4201
491
  console.log(`The app at ${appOrigin2} is not paired with this agent.`);
4202
492
  console.log(`Pair it with (valid for 10 minutes, single use):
@@ -4206,7 +496,7 @@ async function main() {
4206
496
  console.log(divider);
4207
497
  }
4208
498
  });
4209
- const pairUrl = pairLink(pairingCode.code, serveApp2 ? publicUrl || agentUrl : appUrl);
499
+ const pairUrl = pairLink(pairingCode.code, serveApp ? publicUrl || agentUrl : appUrl);
4210
500
  if (offlineFallback) {
4211
501
  console.log(`${DEFAULT_APP_URL === hostedAppUrl ? "planche.dev" : hostedAppUrl} is unreachable \u2014 serving the app bundled with this agent (v${VERSION}) instead.`);
4212
502
  }