hive-am 1.0.0 → 1.2.0

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 (268) hide show
  1. package/CHANGELOG.md +114 -1
  2. package/README.md +34 -4
  3. package/package.json +13 -2
  4. package/server/dist/api.js +42 -677
  5. package/server/dist/connections/addressing.js +87 -0
  6. package/server/dist/connections/channel-skill.js +62 -0
  7. package/server/dist/connections/fake.js +1 -0
  8. package/server/dist/connections/files.js +45 -1
  9. package/server/dist/connections/format.js +9 -1
  10. package/server/dist/connections/manager.js +60 -3
  11. package/server/dist/connections/outbound.js +2 -1
  12. package/server/dist/connections/prompt.js +12 -3
  13. package/server/dist/connections/rate.js +20 -0
  14. package/server/dist/connections/router.js +55 -22
  15. package/server/dist/connections/secrets.js +52 -0
  16. package/server/dist/connections/store.js +6 -43
  17. package/server/dist/connections/telegram.js +105 -14
  18. package/server/dist/cron.js +138 -0
  19. package/server/dist/db/json.js +16 -0
  20. package/server/dist/db/migrations.js +199 -0
  21. package/server/dist/db.js +98 -196
  22. package/server/dist/errors.js +11 -0
  23. package/server/dist/git/browse.js +99 -0
  24. package/server/dist/git/ops.js +136 -19
  25. package/server/dist/git/repo.js +2 -1
  26. package/server/dist/history/claude.js +33 -20
  27. package/server/dist/history/index.js +17 -9
  28. package/server/dist/history/kiro.js +17 -12
  29. package/server/dist/history/lines.js +12 -0
  30. package/server/dist/history/opencode.js +18 -1
  31. package/server/dist/http/body.js +46 -0
  32. package/server/dist/http/errors.js +21 -0
  33. package/server/dist/http/guards.js +44 -0
  34. package/server/dist/http/router.js +24 -0
  35. package/server/dist/http/routes/agents.js +103 -0
  36. package/server/dist/http/routes/channel.js +16 -0
  37. package/server/dist/http/routes/colonies.js +9 -0
  38. package/server/dist/http/routes/connections.js +36 -0
  39. package/server/dist/http/routes/fs.js +22 -0
  40. package/server/dist/http/routes/git.js +90 -0
  41. package/server/dist/http/routes/meta.js +11 -0
  42. package/server/dist/http/routes/notebook.js +34 -0
  43. package/server/dist/http/routes/orchestration.js +32 -0
  44. package/server/dist/http/routes/resources.js +68 -0
  45. package/server/dist/http/routes/schedules.js +40 -0
  46. package/server/dist/http/routes/skills.js +47 -0
  47. package/server/dist/http/routes/terminals.js +18 -0
  48. package/server/dist/http/routes/types.js +36 -0
  49. package/server/dist/http/validate.js +65 -0
  50. package/server/dist/index.js +74 -4
  51. package/server/dist/instructions.js +23 -11
  52. package/server/dist/objects/agent.js +49 -0
  53. package/server/dist/objects/database.js +451 -0
  54. package/server/dist/objects/db-history.js +79 -0
  55. package/server/dist/objects/docker-info.js +75 -0
  56. package/server/dist/objects/docker.js +285 -0
  57. package/server/dist/objects/driver-props.js +196 -0
  58. package/server/dist/objects/exec.js +16 -0
  59. package/server/dist/objects/http-history.js +44 -0
  60. package/server/dist/objects/http-script.js +644 -0
  61. package/server/dist/objects/http.js +735 -0
  62. package/server/dist/objects/index.js +445 -0
  63. package/server/dist/objects/model.js +250 -0
  64. package/server/dist/objects/runner.js +211 -0
  65. package/server/dist/objects/stats.js +106 -0
  66. package/server/dist/objects/store.js +115 -0
  67. package/server/dist/objects/supervisor.js +111 -0
  68. package/server/dist/objects/templates.js +67 -0
  69. package/server/dist/providers/claude.js +5 -13
  70. package/server/dist/providers/index.js +7 -1
  71. package/server/dist/providers/kiro.js +4 -5
  72. package/server/dist/providers/opencode-server.js +164 -0
  73. package/server/dist/providers/opencode.js +60 -37
  74. package/server/dist/providers/preamble.js +2 -0
  75. package/server/dist/providers/spawn.js +31 -3
  76. package/server/dist/runtime.js +10 -7
  77. package/server/dist/schedules.js +222 -0
  78. package/server/dist/seed.js +15 -11
  79. package/server/dist/services/agents.js +90 -0
  80. package/server/dist/services/colonies.js +38 -0
  81. package/server/dist/services/connections.js +76 -0
  82. package/server/dist/services/notebook.js +22 -0
  83. package/server/dist/services/providers.js +27 -0
  84. package/server/dist/services/schedules.js +18 -0
  85. package/server/dist/sessions-summary.js +51 -0
  86. package/server/dist/skills/defaults.js +120 -0
  87. package/server/dist/skills/notebook.js +3 -8
  88. package/server/dist/terminals.js +177 -0
  89. package/server/dist/wake.js +149 -0
  90. package/server/dist/watch.js +201 -0
  91. package/server/mcp/dispatch.mjs +169 -1
  92. package/web/.next/BUILD_ID +1 -1
  93. package/web/.next/app-build-manifest.json +145 -85
  94. package/web/.next/app-path-routes-manifest.json +5 -2
  95. package/web/.next/build-manifest.json +14 -14
  96. package/web/.next/prerender-manifest.json +85 -37
  97. package/web/.next/react-loadable-manifest.json +26 -1
  98. package/web/.next/required-server-files.json +13 -1
  99. package/web/.next/routes-manifest.json +32 -0
  100. package/web/.next/server/app/_not-found/page.js +2 -2
  101. package/web/.next/server/app/_not-found/page.js.nft.json +1 -1
  102. package/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  103. package/web/.next/server/app/_not-found.html +1 -1
  104. package/web/.next/server/app/_not-found.rsc +12 -11
  105. package/web/.next/server/app/agents/[id]/page.js +4 -4
  106. package/web/.next/server/app/agents/[id]/page.js.nft.json +1 -1
  107. package/web/.next/server/app/agents/[id]/page_client-reference-manifest.js +1 -1
  108. package/web/.next/server/app/agents/page.js +2 -2
  109. package/web/.next/server/app/agents/page.js.nft.json +1 -1
  110. package/web/.next/server/app/agents/page_client-reference-manifest.js +1 -1
  111. package/web/.next/server/app/agents.html +1 -1
  112. package/web/.next/server/app/agents.rsc +14 -13
  113. package/web/.next/server/app/connections/page.js +2 -2
  114. package/web/.next/server/app/connections/page.js.nft.json +1 -1
  115. package/web/.next/server/app/connections/page_client-reference-manifest.js +1 -1
  116. package/web/.next/server/app/connections.html +1 -1
  117. package/web/.next/server/app/connections.rsc +14 -13
  118. package/web/.next/server/app/index.html +1 -1
  119. package/web/.next/server/app/index.rsc +14 -13
  120. package/web/.next/server/app/page.js +2 -2
  121. package/web/.next/server/app/page.js.nft.json +1 -1
  122. package/web/.next/server/app/page_client-reference-manifest.js +1 -1
  123. package/web/.next/server/app/relations/page.js +2 -2
  124. package/web/.next/server/app/relations/page.js.nft.json +1 -1
  125. package/web/.next/server/app/relations/page_client-reference-manifest.js +1 -1
  126. package/web/.next/server/app/relations.html +1 -1
  127. package/web/.next/server/app/relations.rsc +14 -13
  128. package/web/.next/server/app/resources/[id]/page.js +2 -0
  129. package/web/.next/server/app/resources/[id]/page.js.nft.json +1 -0
  130. package/web/.next/server/app/resources/[id]/page_client-reference-manifest.js +1 -0
  131. package/web/.next/server/app/resources/page.js +2 -0
  132. package/web/.next/server/app/resources/page.js.nft.json +1 -0
  133. package/web/.next/server/app/resources/page_client-reference-manifest.js +1 -0
  134. package/web/.next/server/app/resources.html +1 -0
  135. package/web/.next/server/app/resources.meta +15 -0
  136. package/web/.next/server/app/resources.rsc +25 -0
  137. package/web/.next/server/app/schedules/page.js +2 -0
  138. package/web/.next/server/app/schedules/page.js.nft.json +1 -0
  139. package/web/.next/server/app/schedules/page_client-reference-manifest.js +1 -0
  140. package/web/.next/server/app/schedules.html +1 -0
  141. package/web/.next/server/app/schedules.meta +15 -0
  142. package/web/.next/server/app/schedules.rsc +25 -0
  143. package/web/.next/server/app/sessions/page.js +2 -2
  144. package/web/.next/server/app/sessions/page.js.nft.json +1 -1
  145. package/web/.next/server/app/sessions/page_client-reference-manifest.js +1 -1
  146. package/web/.next/server/app/sessions.html +1 -1
  147. package/web/.next/server/app/sessions.rsc +14 -13
  148. package/web/.next/server/app/skills/page.js +2 -2
  149. package/web/.next/server/app/skills/page.js.nft.json +1 -1
  150. package/web/.next/server/app/skills/page_client-reference-manifest.js +1 -1
  151. package/web/.next/server/app/skills.html +1 -1
  152. package/web/.next/server/app/skills.rsc +14 -13
  153. package/web/.next/server/app/types/page.js +2 -2
  154. package/web/.next/server/app/types/page.js.nft.json +1 -1
  155. package/web/.next/server/app/types/page_client-reference-manifest.js +1 -1
  156. package/web/.next/server/app/types.html +1 -1
  157. package/web/.next/server/app/types.rsc +14 -13
  158. package/web/.next/server/app-paths-manifest.json +5 -2
  159. package/web/.next/server/chunks/143.js +1 -1
  160. package/web/.next/server/chunks/274.js +1 -0
  161. package/web/.next/server/chunks/306.js +1 -0
  162. package/web/.next/server/chunks/423.js +1 -0
  163. package/web/.next/server/chunks/505.js +6 -0
  164. package/web/.next/server/chunks/555.js +1 -0
  165. package/web/.next/server/chunks/612.js +1 -0
  166. package/web/.next/server/chunks/638.js +1 -0
  167. package/web/.next/server/chunks/692.js +18 -0
  168. package/web/.next/server/chunks/716.js +2 -0
  169. package/web/.next/server/chunks/790.js +11 -0
  170. package/web/.next/server/chunks/802.js +1 -0
  171. package/web/.next/server/chunks/890.js +1 -1
  172. package/web/.next/server/chunks/919.js +1 -0
  173. package/web/.next/server/chunks/950.js +6 -0
  174. package/web/.next/server/middleware-build-manifest.js +1 -1
  175. package/web/.next/server/middleware-react-loadable-manifest.js +1 -1
  176. package/web/.next/server/next-font-manifest.js +1 -1
  177. package/web/.next/server/next-font-manifest.json +1 -1
  178. package/web/.next/server/pages/404.html +1 -1
  179. package/web/.next/server/pages/500.html +1 -1
  180. package/web/.next/server/server-reference-manifest.json +1 -1
  181. package/web/.next/static/chunks/0937d497-114a2a3de515565e.js +1 -0
  182. package/web/.next/static/chunks/176-aeaf907a6ea5627d.js +1 -0
  183. package/web/.next/static/chunks/291-efae53ea5f7ee92d.js +1 -0
  184. package/web/.next/static/chunks/2b9ff8ee-969e65cc354b2150.js +1 -0
  185. package/web/.next/static/chunks/30-9adcbe92b15a6815.js +1 -0
  186. package/web/.next/static/chunks/316-26da9cc233ba7557.js +1 -0
  187. package/web/.next/static/chunks/350-56599dc3bf06066d.js +1 -0
  188. package/web/.next/static/chunks/486-13e08542f3a1cbac.js +1 -0
  189. package/web/.next/static/chunks/491-6b4e6201f5110dff.js +1 -0
  190. package/web/.next/static/chunks/517-ccaf8cc5c733cbd8.js +1 -0
  191. package/web/.next/static/chunks/545.bfbd92b7632c09de.js +1 -0
  192. package/web/.next/static/chunks/{805-d946ebedb7e85f73.js → 562-7c2e33d838e869bf.js} +1 -1
  193. package/web/.next/static/chunks/581-3ad5c4343dad3f50.js +1 -0
  194. package/web/.next/static/chunks/586-f7017d585a84738d.js +1 -0
  195. package/web/.next/static/chunks/657.b4c39e676c3e20dc.js +1 -0
  196. package/web/.next/static/chunks/701.6abcaf5e60463eef.js +6 -0
  197. package/web/.next/static/chunks/705-a16e1fe09be1ac7f.js +1 -0
  198. package/web/.next/static/chunks/706-fa2be0f10ffebe6a.js +1 -0
  199. package/web/.next/static/chunks/837-33c7480524f48577.js +1 -0
  200. package/web/.next/static/chunks/839-23c5508622fae942.js +1 -0
  201. package/web/.next/static/chunks/844-fe55292e0701f436.js +1 -0
  202. package/web/.next/static/chunks/89-b194fcffe5120c44.js +1 -0
  203. package/web/.next/static/chunks/97.594543c2196de524.js +1 -0
  204. package/web/.next/static/chunks/app/_not-found/page-238a7b80bda6b4ae.js +1 -0
  205. package/web/.next/static/chunks/app/agents/[id]/page-4af6c246c4ca7a97.js +1 -0
  206. package/web/.next/static/chunks/app/agents/page-f4c4c80f955f3967.js +1 -0
  207. package/web/.next/static/chunks/app/connections/page-0936beaf02d863c6.js +1 -0
  208. package/web/.next/static/chunks/app/layout-87ebb70bd9cb8443.js +1 -0
  209. package/web/.next/static/chunks/app/page-a7f292117b7d76bf.js +1 -0
  210. package/web/.next/static/chunks/app/relations/page-fba25393c8688714.js +1 -0
  211. package/web/.next/static/chunks/app/resources/[id]/page-d110d89bb8b5fca7.js +1 -0
  212. package/web/.next/static/chunks/app/resources/page-bbd0b450d3f3131d.js +1 -0
  213. package/web/.next/static/chunks/app/schedules/page-a1124397fc760413.js +1 -0
  214. package/web/.next/static/chunks/app/sessions/page-0e322811b0cde724.js +1 -0
  215. package/web/.next/static/chunks/app/skills/page-22ea0ef89d9f7a13.js +1 -0
  216. package/web/.next/static/chunks/app/types/page-18c4e1db2724d41d.js +1 -0
  217. package/web/.next/static/chunks/fcafe1ac.366e4950d3b0d4b7.js +18 -0
  218. package/web/.next/static/chunks/framework-bbac72adbbe2d328.js +1 -0
  219. package/web/.next/static/chunks/main-app-6086068e5fb5eacd.js +1 -0
  220. package/web/.next/static/chunks/main-c15f3fee674e0303.js +1 -0
  221. package/web/.next/static/chunks/pages/_app-d1670a2c6ce392fc.js +1 -0
  222. package/web/.next/static/chunks/pages/_error-95aac014d202fa7a.js +1 -0
  223. package/web/.next/static/chunks/webpack-ef515e41a83a16ac.js +1 -0
  224. package/web/.next/static/css/945795da867f5250.css +1 -0
  225. package/web/.next/static/css/dd4e32c285f75776.css +1 -0
  226. package/web/.next/static/p4S3i9kvO359GW91DQiHu/_buildManifest.js +1 -0
  227. package/web/.next/trace +29 -23
  228. package/web/.next/types/app/resources/[id]/page.ts +84 -0
  229. package/web/.next/types/app/resources/page.ts +84 -0
  230. package/web/.next/types/app/schedules/page.ts +84 -0
  231. package/web/.next/types/routes.d.ts +7 -2
  232. package/web/.next/types/validator.ts +27 -0
  233. package/web/next.config.mjs +4 -0
  234. package/web/.next/server/chunks/149.js +0 -11
  235. package/web/.next/server/chunks/247.js +0 -1
  236. package/web/.next/server/chunks/851.js +0 -1
  237. package/web/.next/server/chunks/932.js +0 -2
  238. package/web/.next/static/84BpRSahg2BTwk3mX6AG8/_buildManifest.js +0 -1
  239. package/web/.next/static/chunks/0937d497-44abb0f9355ace92.js +0 -1
  240. package/web/.next/static/chunks/2b9ff8ee-c3b61ac1e608c57d.js +0 -1
  241. package/web/.next/static/chunks/351-65da8c1a6e25f93b.js +0 -1
  242. package/web/.next/static/chunks/463-6ae35cdefdc59169.js +0 -1
  243. package/web/.next/static/chunks/493-92266798be872b8f.js +0 -1
  244. package/web/.next/static/chunks/517-eb26c72f6059aa26.js +0 -1
  245. package/web/.next/static/chunks/657.f1e03b7b65eba1d9.js +0 -1
  246. package/web/.next/static/chunks/687-3a4205f51be38602.js +0 -1
  247. package/web/.next/static/chunks/837-fd7623601dc78ed8.js +0 -1
  248. package/web/.next/static/chunks/850-318f32484989dd59.js +0 -1
  249. package/web/.next/static/chunks/89-ace2e1aa63babfdf.js +0 -1
  250. package/web/.next/static/chunks/951-e250b541b67da7ce.js +0 -1
  251. package/web/.next/static/chunks/app/_not-found/page-bbd54d7b9a55d7ae.js +0 -1
  252. package/web/.next/static/chunks/app/agents/[id]/page-5eaf094fec246f1b.js +0 -1
  253. package/web/.next/static/chunks/app/agents/page-e9b17f1c3439ef98.js +0 -1
  254. package/web/.next/static/chunks/app/connections/page-c6afd90abd82d47a.js +0 -1
  255. package/web/.next/static/chunks/app/layout-c415d5a847cd94b3.js +0 -1
  256. package/web/.next/static/chunks/app/page-6fc87fc5a128a204.js +0 -1
  257. package/web/.next/static/chunks/app/relations/page-0355a93a8eb67989.js +0 -1
  258. package/web/.next/static/chunks/app/sessions/page-beae9a74a07d69e0.js +0 -1
  259. package/web/.next/static/chunks/app/skills/page-4f839ac913e26d32.js +0 -1
  260. package/web/.next/static/chunks/app/types/page-4f6b99af3232f861.js +0 -1
  261. package/web/.next/static/chunks/framework-0e68884e76d9a8d3.js +0 -1
  262. package/web/.next/static/chunks/main-app-d6f73ccd41f325c5.js +0 -1
  263. package/web/.next/static/chunks/main-cd22509db15764e2.js +0 -1
  264. package/web/.next/static/chunks/pages/_app-0f75d689c738a6fc.js +0 -1
  265. package/web/.next/static/chunks/pages/_error-d481f6d49fb4455d.js +0 -1
  266. package/web/.next/static/chunks/webpack-d40de72e52f65f79.js +0 -1
  267. package/web/.next/static/css/abe1a6216491f944.css +0 -1
  268. /package/web/.next/static/{84BpRSahg2BTwk3mX6AG8 → p4S3i9kvO359GW91DQiHu}/_ssgManifest.js +0 -0
@@ -0,0 +1,451 @@
1
+ import { createRequire } from 'node:module';
2
+ import { existsSync } from 'node:fs';
3
+ import { spawn } from 'node:child_process';
4
+ import { DB_ENGINES, ObjectError } from './model.js';
5
+ import { plain, propValues, tlsOptions } from './driver-props.js';
6
+ const DEFAULT_ROWS = 1000, DEFAULT_TIMEOUT = 30;
7
+ const CONNECT_MS = 8000;
8
+ const CELL_MAX = 10_000;
9
+ /** What the driver says, without the password (drivers sometimes echo the connection string). */
10
+ const redact = (cfg, e) => {
11
+ let m = e instanceof Error ? e.message : String(e);
12
+ if (cfg.password)
13
+ m = m.split(cfg.password).join('***');
14
+ return m.split('\n')[0].slice(0, 300) || 'the database failed';
15
+ };
16
+ /** A value as the interface can show it. */
17
+ export function cell(v) {
18
+ if (v === null || v === undefined)
19
+ return null;
20
+ if (typeof v === 'bigint')
21
+ return v.toString();
22
+ if (typeof v === 'number')
23
+ return Number.isFinite(v) ? v : String(v);
24
+ if (typeof v === 'string')
25
+ return v.length > CELL_MAX ? `${v.slice(0, CELL_MAX)}…` : v;
26
+ if (typeof v === 'boolean')
27
+ return v;
28
+ if (v instanceof Date)
29
+ return Number.isNaN(v.getTime()) ? null : v.toISOString();
30
+ if (Buffer.isBuffer(v) || v instanceof Uint8Array) {
31
+ const b = Buffer.from(v);
32
+ return `0x${b.subarray(0, 64).toString('hex')}${b.length > 64 ? `… (${b.length} bytes)` : ''}`;
33
+ }
34
+ try {
35
+ const t = JSON.stringify(v);
36
+ return t.length > CELL_MAX ? `${t.slice(0, CELL_MAX)}…` : t;
37
+ }
38
+ catch {
39
+ return String(v);
40
+ }
41
+ }
42
+ // ---- read-only check -------------------------------------------------------------------------------------------------------------------
43
+ const READ_START = new Set(['select', 'with', 'show', 'describe', 'desc', 'explain', 'pragma', 'values', 'table']);
44
+ const WRITE_WORDS = /\b(insert|update|delete|merge|drop|alter|create|truncate|grant|revoke|into|attach|detach|vacuum|reindex|exec|execute|call|copy|lock|set|use|replace\s+into)\b/;
45
+ /** The text of a statement without comments and without what is inside quotes: what is left is what the engine will really read. */
46
+ function bare(sql) {
47
+ let out = '', i = 0;
48
+ while (i < sql.length) {
49
+ const c = sql[i], n = sql[i + 1];
50
+ if (c === '-' && n === '-') {
51
+ while (i < sql.length && sql[i] !== '\n')
52
+ i++;
53
+ out += ' ';
54
+ continue;
55
+ }
56
+ if (c === '#') {
57
+ while (i < sql.length && sql[i] !== '\n')
58
+ i++;
59
+ out += ' ';
60
+ continue;
61
+ }
62
+ if (c === '/' && n === '*') {
63
+ const e = sql.indexOf('*/', i + 2);
64
+ i = e < 0 ? sql.length : e + 2;
65
+ out += ' ';
66
+ continue;
67
+ }
68
+ if (c === "'" || c === '"' || c === '`') {
69
+ const q = c;
70
+ i++;
71
+ while (i < sql.length) {
72
+ if (sql[i] === '\\' && q !== '`') {
73
+ i += 2;
74
+ continue;
75
+ }
76
+ if (sql[i] === q) {
77
+ if (sql[i + 1] === q) {
78
+ i += 2;
79
+ continue;
80
+ }
81
+ break;
82
+ }
83
+ i++;
84
+ }
85
+ i++;
86
+ out += ' ""';
87
+ continue;
88
+ }
89
+ if (c === '[') {
90
+ const e = sql.indexOf(']', i);
91
+ i = e < 0 ? sql.length : e + 1;
92
+ out += ' "" ';
93
+ continue;
94
+ } // SQL Server identifier
95
+ out += c;
96
+ i++;
97
+ }
98
+ return out;
99
+ }
100
+ /** Throws unless the text is one statement that only reads. */
101
+ export function assertReadOnly(sql) {
102
+ const t = bare(sql).trim().replace(/;+\s*$/, '').toLowerCase();
103
+ if (!t)
104
+ throw new ObjectError('Write a query');
105
+ if (t.includes(';'))
106
+ throw new ObjectError('Only one statement at a time');
107
+ const first = /^[a-z]+/.exec(t)?.[0] ?? '(';
108
+ if (first !== '(' && !READ_START.has(first))
109
+ throw new ObjectError('This database is read-only: only SELECT, WITH, SHOW, DESCRIBE and EXPLAIN are allowed. Turn read-only off in its settings to write.');
110
+ if (first === 'pragma' && t.includes('='))
111
+ throw new ObjectError('This database is read-only: a PRAGMA cannot change settings.');
112
+ const hit = WRITE_WORDS.exec(first === 'pragma' ? '' : t);
113
+ if (hit)
114
+ throw new ObjectError(`This database is read-only: "${hit[1].toUpperCase()}" is not allowed. Turn read-only off in its settings to write.`);
115
+ }
116
+ // ---- engines ---------------------------------------------------------------------------------------------------------------------------
117
+ const req = createRequire(import.meta.url);
118
+ const fixed = (n, d) => (n && n > 0 ? n : d);
119
+ async function openMysql(cfg, database) {
120
+ const m = await import('mysql2');
121
+ const mysql = m.default ?? m;
122
+ const pv = propValues('mysql', cfg.props);
123
+ const c = mysql.createConnection({
124
+ connectTimeout: CONNECT_MS, ...plain(pv),
125
+ host: cfg.host, port: fixed(cfg.port, DB_ENGINES.mysql.port), user: cfg.user, password: cfg.password, database: database ?? cfg.database,
126
+ ssl: cfg.ssl ? tlsOptions(pv, !cfg.trustCert) : undefined, dateStrings: true, supportBigNumbers: true, bigNumberStrings: true, rowsAsArray: true,
127
+ });
128
+ await new Promise((ok, no) => c.connect((e) => (e ? no(e) : ok())));
129
+ c.on('error', () => undefined);
130
+ let ro = false;
131
+ return {
132
+ async run(sql, o) {
133
+ if (o.readOnly && !ro) {
134
+ await new Promise((ok, no) => c.query('SET SESSION TRANSACTION READ ONLY', (e) => (e ? no(e) : ok())));
135
+ ro = true;
136
+ }
137
+ return new Promise((ok, no) => {
138
+ const rows = [];
139
+ let columns = [], truncated = false, affected, done = false;
140
+ const finish = (err) => { if (done)
141
+ return; done = true; err ? no(err) : ok({ columns, rows, truncated, affected }); };
142
+ const q = c.query({ sql, rowsAsArray: true, timeout: o.timeoutMs });
143
+ q.on('fields', (f) => { columns = (f ?? []).map((x) => ({ name: x.name })); })
144
+ .on('result', (r) => {
145
+ if (Array.isArray(r)) {
146
+ if (rows.length < o.max)
147
+ rows.push(r);
148
+ else if (!truncated) {
149
+ truncated = true;
150
+ c.destroy();
151
+ finish();
152
+ }
153
+ }
154
+ else if (r && typeof r.affectedRows === 'number')
155
+ affected = r.affectedRows;
156
+ })
157
+ .on('error', (e) => finish(truncated ? undefined : e))
158
+ .on('end', () => finish());
159
+ });
160
+ },
161
+ async close() { try {
162
+ c.destroy();
163
+ }
164
+ catch { /* gone */ } },
165
+ };
166
+ }
167
+ async function openPg(cfg, database) {
168
+ const m = await import('pg');
169
+ const pg = m.default ?? m;
170
+ const cm = await import('pg-cursor');
171
+ const Cursor = cm.default ?? cm;
172
+ const pv = propValues('postgresql', cfg.props);
173
+ const { search_path: path, options: extra, ...rest } = plain(pv);
174
+ const options = [extra, path !== undefined ? `-c search_path=${String(path).replace(/ /g, '')}` : undefined].filter(Boolean).join(' ') || undefined;
175
+ const c = new pg.Client({
176
+ connectionTimeoutMillis: CONNECT_MS, ...rest, options,
177
+ host: cfg.host, port: fixed(cfg.port, DB_ENGINES.postgresql.port), user: cfg.user, password: cfg.password, database: database ?? cfg.database ?? undefined,
178
+ ssl: cfg.ssl ? tlsOptions(pv, !cfg.trustCert) : undefined,
179
+ });
180
+ c.on('error', () => undefined);
181
+ await c.connect();
182
+ return {
183
+ async run(sql, o) {
184
+ await c.query(`SET statement_timeout = ${Math.round(o.timeoutMs)}`);
185
+ if (o.readOnly)
186
+ await c.query('BEGIN READ ONLY');
187
+ try {
188
+ const cur = c.query(new Cursor(sql, [], { rowMode: 'array' }));
189
+ const got = await cur.read(o.max + 1);
190
+ const r = cur._result ?? {};
191
+ await cur.close();
192
+ const truncated = got.length > o.max;
193
+ const columns = (r.fields ?? []).map((f) => ({ name: f.name }));
194
+ return { columns, rows: truncated ? got.slice(0, o.max) : got, truncated, affected: columns.length === 0 && typeof r.rowCount === 'number' ? r.rowCount : undefined };
195
+ }
196
+ finally {
197
+ if (o.readOnly)
198
+ await c.query('ROLLBACK').catch(() => undefined);
199
+ }
200
+ },
201
+ async close() { try {
202
+ await c.end();
203
+ }
204
+ catch { /* gone */ } },
205
+ };
206
+ }
207
+ async function openMssql(cfg, database) {
208
+ const m = await import('mssql');
209
+ const mssql = m.default ?? m;
210
+ const pv = propValues('mssql', cfg.props);
211
+ const { connectionTimeout, ...opts } = plain(pv);
212
+ const crypto = cfg.ssl ? tlsOptions(pv, true) : undefined;
213
+ if (crypto)
214
+ delete crypto.rejectUnauthorized;
215
+ const pool = new mssql.ConnectionPool({
216
+ server: cfg.host, user: cfg.user, password: cfg.password, database: database ?? cfg.database,
217
+ // A named instance is found by its name, so it takes no port.
218
+ ...(opts.instanceName !== undefined ? {} : { port: fixed(cfg.port, DB_ENGINES.mssql.port) }),
219
+ options: { ...opts, encrypt: !!cfg.ssl, trustServerCertificate: cfg.ssl ? !!cfg.trustCert : true, ...(crypto && Object.keys(crypto).length ? { cryptoCredentialsDetails: crypto } : {}) },
220
+ connectionTimeout: connectionTimeout ?? CONNECT_MS, pool: { max: 1, min: 0 },
221
+ });
222
+ pool.on('error', () => undefined);
223
+ await pool.connect();
224
+ return {
225
+ async run(sql, o) {
226
+ return new Promise((ok, no) => {
227
+ const r = new mssql.Request(pool);
228
+ r.stream = true;
229
+ r.arrayRowMode = true;
230
+ const rows = [];
231
+ let columns = [], truncated = false, affected, done = false;
232
+ const finish = (err) => { if (done)
233
+ return; done = true; clearTimeout(timer); err ? no(err) : ok({ columns, rows, truncated, affected }); };
234
+ const timer = setTimeout(() => { try {
235
+ r.cancel();
236
+ }
237
+ catch { /* done */ } finish(new Error(`The query took more than ${Math.round(o.timeoutMs / 1000)} s`)); }, o.timeoutMs);
238
+ r.on('recordset', (cols) => { columns = Object.values(cols).sort((a, b) => a.index - b.index).map((x) => ({ name: x.name })); });
239
+ r.on('row', (row) => { if (rows.length < o.max)
240
+ rows.push(row);
241
+ else if (!truncated) {
242
+ truncated = true;
243
+ try {
244
+ r.cancel();
245
+ }
246
+ catch { /* done */ }
247
+ finish();
248
+ } });
249
+ r.on('error', (e) => finish(truncated ? undefined : e));
250
+ r.on('done', (res) => { if (Array.isArray(res?.rowsAffected) && columns.length === 0)
251
+ affected = res.rowsAffected.reduce((a, b) => a + b, 0); finish(); });
252
+ r.query(sql);
253
+ });
254
+ },
255
+ async close() { try {
256
+ await pool.close();
257
+ }
258
+ catch { /* gone */ } },
259
+ };
260
+ }
261
+ /**
262
+ * better-sqlite3 is synchronous and has no way to interrupt a statement, so a query that never ends cannot be stopped from inside
263
+ * the process (not even in a thread: it would go on using a core forever). It runs in a process of its own that is killed at the timeout.
264
+ */
265
+ const SQLITE_CHILD = `
266
+ const w = JSON.parse(require('node:fs').readFileSync(0, 'utf8'));
267
+ const Database = require(w.mod);
268
+ let db, out;
269
+ try {
270
+ db = new Database(w.file, { readonly: w.readOnly, fileMustExist: true, ...(w.timeout !== undefined ? { timeout: w.timeout } : {}) });
271
+ for (const [k, v] of Object.entries(w.pragmas)) db.pragma(k + ' = ' + v);
272
+ const stmt = db.prepare(w.sql);
273
+ if (stmt.reader) {
274
+ const columns = stmt.columns().map((c) => ({ name: c.name })); stmt.raw(true);
275
+ const rows = []; let truncated = false;
276
+ for (const r of stmt.iterate()) { if (rows.length >= w.max) { truncated = true; break; } rows.push(r.map((v) => typeof v === 'bigint' ? v.toString() : (Buffer.isBuffer(v) ? { $blob: v.toString('base64') } : v))); }
277
+ out = { ok: true, columns, rows, truncated };
278
+ } else { const r = stmt.run(); out = { ok: true, columns: [], rows: [], truncated: false, affected: r.changes }; }
279
+ } catch (e) { out = { ok: false, error: String(e && e.message || e) }; }
280
+ try { db && db.close(); } catch {}
281
+ process.stdout.write(JSON.stringify(out));
282
+ `;
283
+ const SQLITE_MAX_OUT = 64 * 1024 * 1024;
284
+ function openSqlite(cfg) {
285
+ if (!existsSync(cfg.file))
286
+ throw new ObjectError('The SQLite file does not exist');
287
+ const mod = req.resolve('better-sqlite3');
288
+ // The values were checked when the object was saved (a number, true/false or one of a few words), so they are safe in a PRAGMA.
289
+ const { timeout, ...rest } = plain(propValues('sqlite', cfg.props));
290
+ const pragmas = Object.fromEntries(Object.entries(rest).map(([k, v]) => [k, typeof v === 'boolean' ? (v ? 1 : 0) : v]));
291
+ return {
292
+ run: (sql, o) => new Promise((ok, no) => {
293
+ const child = spawn(process.execPath, ['-e', SQLITE_CHILD], { stdio: ['pipe', 'pipe', 'ignore'] });
294
+ const chunks = [];
295
+ let size = 0, done = false;
296
+ const finish = (err, v) => { if (done)
297
+ return; done = true; clearTimeout(timer); child.kill('SIGKILL'); err ? no(err) : ok(v); };
298
+ const timer = setTimeout(() => finish(new Error(`The query took more than ${Math.round(o.timeoutMs / 1000)} s`)), o.timeoutMs);
299
+ child.stdout.on('data', (c) => { size += c.length; if (size > SQLITE_MAX_OUT)
300
+ finish(new Error('The result is too big: use fewer rows or columns'));
301
+ else
302
+ chunks.push(c); });
303
+ child.once('error', (e) => finish(e));
304
+ child.once('close', () => {
305
+ if (done)
306
+ return;
307
+ try {
308
+ const m = JSON.parse(Buffer.concat(chunks).toString('utf8'));
309
+ m.ok ? finish(undefined, { columns: m.columns, rows: m.rows.map((r) => r.map((v) => (v && typeof v === 'object' && '$blob' in v ? Buffer.from(v.$blob, 'base64') : v))), truncated: m.truncated, affected: m.affected }) : finish(new Error(m.error));
310
+ }
311
+ catch {
312
+ finish(new Error('The SQLite process ended without an answer'));
313
+ }
314
+ });
315
+ child.stdin.end(JSON.stringify({ mod, file: cfg.file, sql, max: o.max, readOnly: o.readOnly, timeout, pragmas }));
316
+ }),
317
+ close: async () => undefined,
318
+ };
319
+ }
320
+ async function open(cfg, database) {
321
+ if (cfg.engine === 'sqlite')
322
+ return openSqlite(cfg);
323
+ if (cfg.engine === 'mysql')
324
+ return openMysql(cfg, database);
325
+ if (cfg.engine === 'postgresql')
326
+ return openPg(cfg, database);
327
+ return openMssql(cfg, database);
328
+ }
329
+ const optsOf = (cfg, readOnly) => ({ max: cfg.maxRows ?? DEFAULT_ROWS, timeoutMs: (cfg.timeoutSec ?? DEFAULT_TIMEOUT) * 1000, readOnly: readOnly ?? cfg.readOnly !== false });
330
+ /** Opens a connection, runs what `fn` asks and closes it, whatever happens. Errors come out as `ObjectError` without the password. */
331
+ async function withConn(cfg, database, fn) {
332
+ let c;
333
+ try {
334
+ c = await open(cfg, database);
335
+ return await fn(c);
336
+ }
337
+ catch (e) {
338
+ throw e instanceof ObjectError ? e : new ObjectError(redact(cfg, e));
339
+ }
340
+ finally {
341
+ await c?.close();
342
+ }
343
+ }
344
+ // ---- what the object offers ------------------------------------------------------------------------------------------------------------
345
+ export async function dbQuery(cfg, p) {
346
+ if (typeof p.sql !== 'string' || !p.sql.trim())
347
+ throw new ObjectError('Write a query');
348
+ if (p.sql.length > 200_000)
349
+ throw new ObjectError('The query is too long');
350
+ const readOnly = p.readOnly ?? cfg.readOnly !== false;
351
+ if (readOnly)
352
+ assertReadOnly(p.sql);
353
+ const t0 = Date.now();
354
+ const r = await withConn(cfg, p.database, (c) => c.run(p.sql, optsOf(cfg, readOnly)));
355
+ return { ...r, rows: r.rows.map((row) => row.map(cell)), ms: Date.now() - t0 };
356
+ }
357
+ const lit = (s) => `'${s.replace(/\\/g, '\\\\').replace(/'/g, "''")}'`;
358
+ const NAME_OK = /^[^\0\r\n]{1,256}$/;
359
+ const name = (v, what) => { const s = typeof v === 'string' ? v : ''; if (!NAME_OK.test(s))
360
+ throw new ObjectError(`${what} is not valid`); return s; };
361
+ const internal = (cfg) => ({ max: 5000, timeoutMs: Math.min(30_000, (cfg.timeoutSec ?? DEFAULT_TIMEOUT) * 1000), readOnly: true });
362
+ export async function dbVersion(cfg, database) {
363
+ const sql = cfg.engine === 'mysql' ? 'SELECT VERSION()' : cfg.engine === 'postgresql' ? 'SHOW server_version' : cfg.engine === 'mssql' ? "SELECT 'SQL Server ' + CAST(SERVERPROPERTY('ProductVersion') AS varchar(50))" : 'SELECT sqlite_version()';
364
+ return withConn(cfg, database, async (c) => {
365
+ const v = String((await c.run(sql, internal(cfg))).rows[0]?.[0] ?? '').split('\n')[0].replace(/\s+/g, ' ').trim();
366
+ const label = cfg.engine === 'sqlite' ? 'SQLite' : cfg.engine === 'postgresql' ? 'PostgreSQL' : cfg.engine === 'mssql' ? '' : /maria/i.test(v) ? 'MariaDB' : 'MySQL';
367
+ return `${label} ${v}`.trim().slice(0, 90);
368
+ });
369
+ }
370
+ export async function dbTest(cfg) {
371
+ const t0 = Date.now();
372
+ const version = await dbVersion(cfg);
373
+ return { ok: true, version, ms: Date.now() - t0 };
374
+ }
375
+ const SYSTEM_DBS = new Set(['information_schema', 'mysql', 'performance_schema', 'sys', 'master', 'tempdb', 'model', 'msdb', 'template0', 'template1']);
376
+ export async function dbSchema(cfg, database) {
377
+ if (database !== undefined)
378
+ name(database, 'The database');
379
+ return withConn(cfg, database, async (c) => {
380
+ const q = async (sql) => (await c.run(sql, internal(cfg))).rows;
381
+ if (cfg.engine === 'sqlite') {
382
+ const rows = await q("SELECT name, type FROM sqlite_master WHERE type IN ('table','view') AND name NOT LIKE 'sqlite_%' ORDER BY name");
383
+ return { engine: cfg.engine, databases: [], database: 'main', tables: rows.map((r) => ({ schema: 'main', name: String(r[0]), type: r[1] === 'view' ? 'view' : 'table' })) };
384
+ }
385
+ if (cfg.engine === 'mysql') {
386
+ const databases = (await q('SHOW DATABASES')).map((r) => String(r[0])).filter((d) => !SYSTEM_DBS.has(d.toLowerCase()));
387
+ const current = database ?? cfg.database ?? String((await q('SELECT DATABASE()'))[0]?.[0] ?? '') ?? '';
388
+ const cur = current || databases[0] || '';
389
+ const tables = cur ? (await q(`SELECT table_name, table_type, table_rows FROM information_schema.tables WHERE table_schema = ${lit(cur)} ORDER BY table_name`)).map((r) => ({ schema: cur, name: String(r[0]), type: /view/i.test(String(r[1])) ? 'view' : 'table', rows: r[2] === null ? undefined : Number(r[2]) })) : [];
390
+ return { engine: cfg.engine, databases: databases.includes(cur) || !cur ? databases : [cur, ...databases], database: cur, tables };
391
+ }
392
+ if (cfg.engine === 'postgresql') {
393
+ const databases = (await q('SELECT datname FROM pg_database WHERE NOT datistemplate AND datallowconn ORDER BY 1')).map((r) => String(r[0]));
394
+ const current = String((await q('SELECT current_database()'))[0]?.[0] ?? '');
395
+ const tables = (await q(`SELECT t.table_schema, t.table_name, t.table_type, c.reltuples::bigint FROM information_schema.tables t
396
+ LEFT JOIN pg_class c ON c.relname = t.table_name AND c.relnamespace = (SELECT oid FROM pg_namespace WHERE nspname = t.table_schema)
397
+ WHERE t.table_schema NOT IN ('pg_catalog','information_schema') ORDER BY 1, 2`)).map((r) => ({ schema: String(r[0]), name: String(r[1]), type: /view/i.test(String(r[2])) ? 'view' : 'table', rows: r[3] === null || Number(r[3]) < 0 ? undefined : Number(r[3]) }));
398
+ return { engine: cfg.engine, databases, database: current, tables };
399
+ }
400
+ const databases = (await q('SELECT name FROM sys.databases WHERE database_id > 4 ORDER BY name').catch(() => [])).map((r) => String(r[0]));
401
+ const current = String((await q('SELECT DB_NAME()'))[0]?.[0] ?? '');
402
+ const tables = (await q('SELECT TABLE_SCHEMA, TABLE_NAME, TABLE_TYPE FROM INFORMATION_SCHEMA.TABLES ORDER BY 1, 2')).map((r) => ({ schema: String(r[0]), name: String(r[1]), type: /view/i.test(String(r[2])) ? 'view' : 'table' }));
403
+ return { engine: cfg.engine, databases: databases.includes(current) ? databases : [current, ...databases], database: current, tables };
404
+ });
405
+ }
406
+ export async function dbColumns(cfg, p) {
407
+ const schema = name(p.schema, 'The schema'), table = name(p.table, 'The table');
408
+ if (p.database !== undefined)
409
+ name(p.database, 'The database');
410
+ return withConn(cfg, p.database, async (c) => {
411
+ const q = async (sql) => (await c.run(sql, internal(cfg))).rows;
412
+ if (cfg.engine === 'sqlite') {
413
+ return (await q(`PRAGMA table_info("${table.replace(/"/g, '""')}")`)).map((r) => ({ name: String(r[1]), type: String(r[2] || ''), nullable: !r[3] && !r[5], key: r[5] ? 'PRI' : undefined, default: r[4] === null ? null : String(r[4]) }));
414
+ }
415
+ if (cfg.engine === 'mysql') {
416
+ return (await q(`SELECT column_name, column_type, is_nullable, column_key, column_default FROM information_schema.columns WHERE table_schema = ${lit(schema)} AND table_name = ${lit(table)} ORDER BY ordinal_position`))
417
+ .map((r) => ({ name: String(r[0]), type: String(r[1]), nullable: r[2] === 'YES', key: r[3] ? String(r[3]) : undefined, default: r[4] === null ? null : String(r[4]) }));
418
+ }
419
+ if (cfg.engine === 'postgresql') {
420
+ return (await q(`SELECT c.column_name, c.data_type, c.is_nullable,
421
+ (SELECT 'PRI' FROM information_schema.table_constraints tc JOIN information_schema.key_column_usage k ON k.constraint_name = tc.constraint_name AND k.table_schema = tc.table_schema
422
+ WHERE tc.constraint_type = 'PRIMARY KEY' AND tc.table_schema = c.table_schema AND tc.table_name = c.table_name AND k.column_name = c.column_name LIMIT 1), c.column_default
423
+ FROM information_schema.columns c WHERE c.table_schema = ${lit(schema)} AND c.table_name = ${lit(table)} ORDER BY c.ordinal_position`))
424
+ .map((r) => ({ name: String(r[0]), type: String(r[1]), nullable: r[2] === 'YES', key: r[3] ? String(r[3]) : undefined, default: r[4] === null ? null : String(r[4]) }));
425
+ }
426
+ return (await q(`SELECT COLUMN_NAME, DATA_TYPE, IS_NULLABLE, COLUMN_DEFAULT FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = ${lit(schema)} AND TABLE_NAME = ${lit(table)} ORDER BY ORDINAL_POSITION`))
427
+ .map((r) => ({ name: String(r[0]), type: String(r[1]), nullable: r[2] === 'YES', default: r[3] === null ? null : String(r[3]) }));
428
+ });
429
+ }
430
+ // ---- state -----------------------------------------------------------------------------------------------------------------------------
431
+ const seen = new Map();
432
+ const FRESH_MS = 15_000;
433
+ export const forgetDatabase = (id) => { seen.delete(id); };
434
+ /** `ready` when a connection opens (with the version and how long it took), an error with the reason when it does not. Asked at most every 15 s. */
435
+ export async function dbState(id, cfg) {
436
+ const hit = seen.get(id);
437
+ if (hit && Date.now() - hit.at < FRESH_MS)
438
+ return hit.state;
439
+ let state;
440
+ try {
441
+ if (cfg.engine === 'sqlite' && !existsSync(cfg.file))
442
+ throw new ObjectError('The file does not exist');
443
+ const r = await dbTest(cfg);
444
+ state = { status: 'ready', detail: `${r.version} · ${r.ms} ms` };
445
+ }
446
+ catch (e) {
447
+ state = { status: 'error', detail: e instanceof Error ? e.message : 'unreachable' };
448
+ }
449
+ seen.set(id, { at: Date.now(), state });
450
+ return state;
451
+ }
@@ -0,0 +1,79 @@
1
+ import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { randomUUID } from 'node:crypto';
4
+ import { DATA_DIR } from '../db.js';
5
+ const HISTORY_MAX = 200, SQL_MAX = 20_000, SAVED_MAX = 100;
6
+ const dirOf = (kind) => join(DATA_DIR, kind);
7
+ const fileOf = (kind, id) => join(dirOf(kind), `${id.replace(/[^\w-]/g, '')}.json`);
8
+ const cache = new Map();
9
+ function load(kind, id) {
10
+ const key = `${kind}/${id}`;
11
+ let list = cache.get(key);
12
+ if (!list) {
13
+ try {
14
+ list = existsSync(fileOf(kind, id)) ? JSON.parse(readFileSync(fileOf(kind, id), 'utf8')) : [];
15
+ }
16
+ catch {
17
+ list = [];
18
+ }
19
+ cache.set(key, list);
20
+ }
21
+ return list;
22
+ }
23
+ function save(kind, id, list) {
24
+ try {
25
+ mkdirSync(dirOf(kind), { recursive: true });
26
+ const f = fileOf(kind, id), tmp = `${f}.tmp`;
27
+ writeFileSync(tmp, JSON.stringify(list));
28
+ renameSync(tmp, f);
29
+ }
30
+ catch { /* a convenience: never breaks a query */ }
31
+ }
32
+ export function recordQuery(id, e) {
33
+ const list = load('db-history', id);
34
+ list.unshift({ ...e, sql: e.sql.length > SQL_MAX ? e.sql.slice(0, SQL_MAX) : e.sql, n: (list[0]?.n ?? 0) + 1, at: Date.now() });
35
+ if (list.length > HISTORY_MAX)
36
+ list.length = HISTORY_MAX;
37
+ save('db-history', id, list);
38
+ }
39
+ export const listQueries = (id) => load('db-history', id);
40
+ export const clearQueries = (id) => { cache.delete(`db-history/${id}`); try {
41
+ rmSync(fileOf('db-history', id), { force: true });
42
+ }
43
+ catch { /* none */ } };
44
+ export const listSaved = (id) => load('db-saved', id);
45
+ export function addSaved(id, p) {
46
+ const list = load('db-saved', id);
47
+ if (list.length >= SAVED_MAX)
48
+ throw new Error(`At most ${SAVED_MAX} saved queries`);
49
+ const e = { id: randomUUID(), name: p.name, sql: p.sql.slice(0, SQL_MAX), database: p.database, at: Date.now() };
50
+ list.unshift(e);
51
+ save('db-saved', id, list);
52
+ return e;
53
+ }
54
+ /** Renames a saved query and/or replaces its text (and database); it keeps its place in the list. */
55
+ export function updateSaved(id, savedId, p) {
56
+ const list = load('db-saved', id), e = list.find((x) => x.id === savedId);
57
+ if (!e)
58
+ return undefined;
59
+ if (p.name !== undefined)
60
+ e.name = p.name;
61
+ if (p.sql !== undefined)
62
+ e.sql = p.sql.slice(0, SQL_MAX);
63
+ if (p.database !== undefined)
64
+ e.database = p.database ?? undefined;
65
+ save('db-saved', id, list);
66
+ return e;
67
+ }
68
+ export function removeSaved(id, savedId) {
69
+ const list = load('db-saved', id), i = list.findIndex((x) => x.id === savedId);
70
+ if (i < 0)
71
+ return false;
72
+ list.splice(i, 1);
73
+ save('db-saved', id, list);
74
+ return true;
75
+ }
76
+ export function forgetQueries(id) { clearQueries(id); cache.delete(`db-saved/${id}`); try {
77
+ rmSync(fileOf('db-saved', id), { force: true });
78
+ }
79
+ catch { /* none */ } }
@@ -0,0 +1,75 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { basename, dirname } from 'node:path';
3
+ import { execCapture } from './exec.js';
4
+ import { ObjectError } from './model.js';
5
+ /**
6
+ * What Docker knows, to suggest in the forms: the containers on this machine, the images, and the services of a compose file. Read-only;
7
+ * when Docker is not available the lists are empty and say why.
8
+ */
9
+ const run = async (args, o = {}) => {
10
+ const r = await execCapture('docker', args, { cwd: o.cwd, timeout: o.timeout ?? 15_000, env: { ...process.env, DOCKER_CLI_HINTS: 'false' } });
11
+ return { out: r.out, err: r.missing ? 'Docker is not installed or not in the PATH' : r.err, code: r.code };
12
+ };
13
+ const lines = (out) => out.trim().split('\n').flatMap((l) => { try {
14
+ return [JSON.parse(l)];
15
+ }
16
+ catch {
17
+ return [];
18
+ } });
19
+ export async function listContainers() {
20
+ const r = await run(['ps', '-a', '--format', '{{json .}}']);
21
+ if (r.code !== 0)
22
+ return { available: false, error: r.err.trim().split('\n').pop()?.slice(0, 200), containers: [] };
23
+ const containers = lines(r.out).map((j) => ({ id: String(j.ID), name: String(j.Names), image: String(j.Image), state: String(j.State), status: String(j.Status), ports: String(j.Ports ?? ''), managed: /(^|,)hive-am\.object=/.test(String(j.Labels ?? '')) }));
24
+ return { available: true, containers: containers.sort((a, b) => a.name.localeCompare(b.name)) };
25
+ }
26
+ export async function listImages() {
27
+ const r = await run(['images', '--format', '{{json .}}']);
28
+ if (r.code !== 0)
29
+ return { available: false, error: r.err.trim().split('\n').pop()?.slice(0, 200), images: [] };
30
+ const images = lines(r.out).filter((j) => j.Repository && j.Repository !== '<none>').map((j) => ({ ref: j.Tag && j.Tag !== '<none>' ? `${j.Repository}:${j.Tag}` : String(j.Repository), id: String(j.ID), size: String(j.Size ?? ''), created: String(j.CreatedSince ?? '') }));
31
+ return { available: true, images: [...new Map(images.map((i) => [i.ref, i])).values()].sort((a, b) => a.ref.localeCompare(b.ref)) };
32
+ }
33
+ /** Without Docker's help: the keys under a top-level `services:` (enough for ordinary compose files). */
34
+ function servicesByHand(text) {
35
+ const out = [];
36
+ let inServices = false, indent = -1;
37
+ for (const raw of text.split('\n')) {
38
+ const line = raw.replace(/\s+#.*$/, '').replace(/\t/g, ' ');
39
+ if (!line.trim() || line.trim().startsWith('#'))
40
+ continue;
41
+ if (/^services:\s*$/.test(line)) {
42
+ inServices = true;
43
+ indent = -1;
44
+ continue;
45
+ }
46
+ if (!inServices)
47
+ continue;
48
+ const m = /^(\s*)([A-Za-z0-9._-]+):\s*(.*)$/.exec(line);
49
+ if (!m)
50
+ continue;
51
+ if (m[1].length === 0)
52
+ break; // another top-level key ends the block
53
+ if (indent < 0)
54
+ indent = m[1].length;
55
+ if (m[1].length === indent)
56
+ out.push(m[2]);
57
+ }
58
+ return out;
59
+ }
60
+ export async function composeServices(file) {
61
+ if (!file.startsWith('/') || !/\.(ya?ml)$/i.test(file))
62
+ throw new ObjectError('Choose a compose file (.yml or .yaml)');
63
+ if (!existsSync(file))
64
+ throw new ObjectError(`The file ${file} does not exist`);
65
+ const project = basename(dirname(file)).toLowerCase().replace(/[^a-z0-9_-]/g, '').replace(/^[^a-z0-9]+/, '') || 'project';
66
+ const r = await run(['compose', '-f', file, 'config', '--services'], { cwd: dirname(file) });
67
+ if (r.code === 0)
68
+ return { services: r.out.trim().split('\n').filter(Boolean).sort(), project, source: 'docker' };
69
+ let text = '';
70
+ try {
71
+ text = readFileSync(file, 'utf8');
72
+ }
73
+ catch { /* unreadable */ }
74
+ return { services: servicesByHand(text).sort(), project, source: 'file', error: r.err.trim().split('\n').pop()?.slice(0, 200) };
75
+ }