@sonde/packs 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (359) hide show
  1. package/.turbo/turbo-build.log +6 -0
  2. package/.turbo/turbo-test.log +814 -0
  3. package/.turbo/turbo-typecheck.log +4 -0
  4. package/CHANGELOG.md +10 -0
  5. package/dist/index.d.ts +16 -1
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +40 -2
  8. package/dist/index.js.map +1 -1
  9. package/dist/integrations/citrix.d.ts +13 -0
  10. package/dist/integrations/citrix.d.ts.map +1 -0
  11. package/dist/integrations/citrix.js +420 -0
  12. package/dist/integrations/citrix.js.map +1 -0
  13. package/dist/integrations/citrix.test.d.ts +2 -0
  14. package/dist/integrations/citrix.test.d.ts.map +1 -0
  15. package/dist/integrations/citrix.test.js +464 -0
  16. package/dist/integrations/citrix.test.js.map +1 -0
  17. package/dist/integrations/graph.d.ts +9 -0
  18. package/dist/integrations/graph.d.ts.map +1 -0
  19. package/dist/integrations/graph.js +290 -0
  20. package/dist/integrations/graph.js.map +1 -0
  21. package/dist/integrations/graph.test.d.ts +2 -0
  22. package/dist/integrations/graph.test.d.ts.map +1 -0
  23. package/dist/integrations/graph.test.js +356 -0
  24. package/dist/integrations/graph.test.js.map +1 -0
  25. package/dist/integrations/httpbin.d.ts +3 -0
  26. package/dist/integrations/httpbin.d.ts.map +1 -0
  27. package/dist/integrations/httpbin.js +70 -0
  28. package/dist/integrations/httpbin.js.map +1 -0
  29. package/dist/integrations/nutanix.d.ts +18 -0
  30. package/dist/integrations/nutanix.d.ts.map +1 -0
  31. package/dist/integrations/nutanix.js +1121 -0
  32. package/dist/integrations/nutanix.js.map +1 -0
  33. package/dist/integrations/nutanix.test.d.ts +2 -0
  34. package/dist/integrations/nutanix.test.d.ts.map +1 -0
  35. package/dist/integrations/nutanix.test.js +978 -0
  36. package/dist/integrations/nutanix.test.js.map +1 -0
  37. package/dist/integrations/proxmox.d.ts +12 -0
  38. package/dist/integrations/proxmox.d.ts.map +1 -0
  39. package/dist/integrations/proxmox.js +733 -0
  40. package/dist/integrations/proxmox.js.map +1 -0
  41. package/dist/integrations/proxmox.test.d.ts +2 -0
  42. package/dist/integrations/proxmox.test.d.ts.map +1 -0
  43. package/dist/integrations/proxmox.test.js +697 -0
  44. package/dist/integrations/proxmox.test.js.map +1 -0
  45. package/dist/integrations/servicenow.d.ts +3 -0
  46. package/dist/integrations/servicenow.d.ts.map +1 -0
  47. package/dist/integrations/servicenow.js +257 -0
  48. package/dist/integrations/servicenow.js.map +1 -0
  49. package/dist/integrations/servicenow.test.d.ts +2 -0
  50. package/dist/integrations/servicenow.test.d.ts.map +1 -0
  51. package/dist/integrations/servicenow.test.js +217 -0
  52. package/dist/integrations/servicenow.test.js.map +1 -0
  53. package/dist/integrations/splunk.d.ts +9 -0
  54. package/dist/integrations/splunk.d.ts.map +1 -0
  55. package/dist/integrations/splunk.js +242 -0
  56. package/dist/integrations/splunk.js.map +1 -0
  57. package/dist/integrations/splunk.test.d.ts +2 -0
  58. package/dist/integrations/splunk.test.d.ts.map +1 -0
  59. package/dist/integrations/splunk.test.js +323 -0
  60. package/dist/integrations/splunk.test.js.map +1 -0
  61. package/dist/mysql/index.d.ts +3 -0
  62. package/dist/mysql/index.d.ts.map +1 -0
  63. package/dist/mysql/index.js +13 -0
  64. package/dist/mysql/index.js.map +1 -0
  65. package/dist/mysql/manifest.d.ts +3 -0
  66. package/dist/mysql/manifest.d.ts.map +1 -0
  67. package/dist/mysql/manifest.js +69 -0
  68. package/dist/mysql/manifest.js.map +1 -0
  69. package/dist/mysql/probes/databases-list.d.ts +13 -0
  70. package/dist/mysql/probes/databases-list.d.ts.map +1 -0
  71. package/dist/mysql/probes/databases-list.js +31 -0
  72. package/dist/mysql/probes/databases-list.js.map +1 -0
  73. package/dist/mysql/probes/databases-list.test.d.ts +2 -0
  74. package/dist/mysql/probes/databases-list.test.d.ts.map +1 -0
  75. package/dist/mysql/probes/databases-list.test.js +54 -0
  76. package/dist/mysql/probes/databases-list.test.js.map +1 -0
  77. package/dist/mysql/probes/processlist.d.ts +18 -0
  78. package/dist/mysql/probes/processlist.d.ts.map +1 -0
  79. package/dist/mysql/probes/processlist.js +36 -0
  80. package/dist/mysql/probes/processlist.js.map +1 -0
  81. package/dist/mysql/probes/processlist.test.d.ts +2 -0
  82. package/dist/mysql/probes/processlist.test.d.ts.map +1 -0
  83. package/dist/mysql/probes/processlist.test.js +41 -0
  84. package/dist/mysql/probes/processlist.test.js.map +1 -0
  85. package/dist/mysql/probes/status.d.ts +14 -0
  86. package/dist/mysql/probes/status.d.ts.map +1 -0
  87. package/dist/mysql/probes/status.js +40 -0
  88. package/dist/mysql/probes/status.js.map +1 -0
  89. package/dist/mysql/probes/status.test.d.ts +2 -0
  90. package/dist/mysql/probes/status.test.d.ts.map +1 -0
  91. package/dist/mysql/probes/status.test.js +43 -0
  92. package/dist/mysql/probes/status.test.js.map +1 -0
  93. package/dist/nginx/index.d.ts +3 -0
  94. package/dist/nginx/index.d.ts.map +1 -0
  95. package/dist/nginx/index.js +13 -0
  96. package/dist/nginx/index.js.map +1 -0
  97. package/dist/nginx/manifest.d.ts +3 -0
  98. package/dist/nginx/manifest.d.ts.map +1 -0
  99. package/dist/nginx/manifest.js +68 -0
  100. package/dist/nginx/manifest.js.map +1 -0
  101. package/dist/nginx/probes/access-log-tail.d.ts +9 -0
  102. package/dist/nginx/probes/access-log-tail.d.ts.map +1 -0
  103. package/dist/nginx/probes/access-log-tail.js +14 -0
  104. package/dist/nginx/probes/access-log-tail.js.map +1 -0
  105. package/dist/nginx/probes/access-log-tail.test.d.ts +2 -0
  106. package/dist/nginx/probes/access-log-tail.test.d.ts.map +1 -0
  107. package/dist/nginx/probes/access-log-tail.test.js +40 -0
  108. package/dist/nginx/probes/access-log-tail.test.js.map +1 -0
  109. package/dist/nginx/probes/config-test.d.ts +8 -0
  110. package/dist/nginx/probes/config-test.d.ts.map +1 -0
  111. package/dist/nginx/probes/config-test.js +18 -0
  112. package/dist/nginx/probes/config-test.js.map +1 -0
  113. package/dist/nginx/probes/config-test.test.d.ts +2 -0
  114. package/dist/nginx/probes/config-test.test.d.ts.map +1 -0
  115. package/dist/nginx/probes/config-test.test.js +35 -0
  116. package/dist/nginx/probes/config-test.test.js.map +1 -0
  117. package/dist/nginx/probes/error-log-tail.d.ts +9 -0
  118. package/dist/nginx/probes/error-log-tail.d.ts.map +1 -0
  119. package/dist/nginx/probes/error-log-tail.js +14 -0
  120. package/dist/nginx/probes/error-log-tail.js.map +1 -0
  121. package/dist/nginx/probes/error-log-tail.test.d.ts +2 -0
  122. package/dist/nginx/probes/error-log-tail.test.d.ts.map +1 -0
  123. package/dist/nginx/probes/error-log-tail.test.js +34 -0
  124. package/dist/nginx/probes/error-log-tail.test.js.map +1 -0
  125. package/dist/postgres/index.d.ts +3 -0
  126. package/dist/postgres/index.d.ts.map +1 -0
  127. package/dist/postgres/index.js +13 -0
  128. package/dist/postgres/index.js.map +1 -0
  129. package/dist/postgres/manifest.d.ts +3 -0
  130. package/dist/postgres/manifest.d.ts.map +1 -0
  131. package/dist/postgres/manifest.js +90 -0
  132. package/dist/postgres/manifest.js.map +1 -0
  133. package/dist/postgres/probes/connections-active.d.ts +17 -0
  134. package/dist/postgres/probes/connections-active.d.ts.map +1 -0
  135. package/dist/postgres/probes/connections-active.js +37 -0
  136. package/dist/postgres/probes/connections-active.js.map +1 -0
  137. package/dist/postgres/probes/connections-active.test.d.ts +2 -0
  138. package/dist/postgres/probes/connections-active.test.d.ts.map +1 -0
  139. package/dist/postgres/probes/connections-active.test.js +36 -0
  140. package/dist/postgres/probes/connections-active.test.js.map +1 -0
  141. package/dist/postgres/probes/databases-list.d.ts +14 -0
  142. package/dist/postgres/probes/databases-list.d.ts.map +1 -0
  143. package/dist/postgres/probes/databases-list.js +34 -0
  144. package/dist/postgres/probes/databases-list.js.map +1 -0
  145. package/dist/postgres/probes/databases-list.test.d.ts +2 -0
  146. package/dist/postgres/probes/databases-list.test.d.ts.map +1 -0
  147. package/dist/postgres/probes/databases-list.test.js +49 -0
  148. package/dist/postgres/probes/databases-list.test.js.map +1 -0
  149. package/dist/postgres/probes/query-slow.d.ts +17 -0
  150. package/dist/postgres/probes/query-slow.d.ts.map +1 -0
  151. package/dist/postgres/probes/query-slow.js +37 -0
  152. package/dist/postgres/probes/query-slow.js.map +1 -0
  153. package/dist/postgres/probes/query-slow.test.d.ts +2 -0
  154. package/dist/postgres/probes/query-slow.test.d.ts.map +1 -0
  155. package/dist/postgres/probes/query-slow.test.js +30 -0
  156. package/dist/postgres/probes/query-slow.test.js.map +1 -0
  157. package/dist/proxmox/index.d.ts +3 -0
  158. package/dist/proxmox/index.d.ts.map +1 -0
  159. package/dist/proxmox/index.js +23 -0
  160. package/dist/proxmox/index.js.map +1 -0
  161. package/dist/proxmox/manifest.d.ts +3 -0
  162. package/dist/proxmox/manifest.d.ts.map +1 -0
  163. package/dist/proxmox/manifest.js +75 -0
  164. package/dist/proxmox/manifest.js.map +1 -0
  165. package/dist/proxmox/probes/ceph-status.d.ts +36 -0
  166. package/dist/proxmox/probes/ceph-status.d.ts.map +1 -0
  167. package/dist/proxmox/probes/ceph-status.js +71 -0
  168. package/dist/proxmox/probes/ceph-status.js.map +1 -0
  169. package/dist/proxmox/probes/ceph-status.test.d.ts +2 -0
  170. package/dist/proxmox/probes/ceph-status.test.d.ts.map +1 -0
  171. package/dist/proxmox/probes/ceph-status.test.js +115 -0
  172. package/dist/proxmox/probes/ceph-status.test.js.map +1 -0
  173. package/dist/proxmox/probes/cluster-config.d.ts +31 -0
  174. package/dist/proxmox/probes/cluster-config.d.ts.map +1 -0
  175. package/dist/proxmox/probes/cluster-config.js +72 -0
  176. package/dist/proxmox/probes/cluster-config.js.map +1 -0
  177. package/dist/proxmox/probes/cluster-config.test.d.ts +2 -0
  178. package/dist/proxmox/probes/cluster-config.test.d.ts.map +1 -0
  179. package/dist/proxmox/probes/cluster-config.test.js +107 -0
  180. package/dist/proxmox/probes/cluster-config.test.js.map +1 -0
  181. package/dist/proxmox/probes/ha-status.d.ts +18 -0
  182. package/dist/proxmox/probes/ha-status.d.ts.map +1 -0
  183. package/dist/proxmox/probes/ha-status.js +38 -0
  184. package/dist/proxmox/probes/ha-status.js.map +1 -0
  185. package/dist/proxmox/probes/ha-status.test.d.ts +2 -0
  186. package/dist/proxmox/probes/ha-status.test.d.ts.map +1 -0
  187. package/dist/proxmox/probes/ha-status.test.js +66 -0
  188. package/dist/proxmox/probes/ha-status.test.js.map +1 -0
  189. package/dist/proxmox/probes/lvm.d.ts +35 -0
  190. package/dist/proxmox/probes/lvm.d.ts.map +1 -0
  191. package/dist/proxmox/probes/lvm.js +75 -0
  192. package/dist/proxmox/probes/lvm.js.map +1 -0
  193. package/dist/proxmox/probes/lvm.test.d.ts +2 -0
  194. package/dist/proxmox/probes/lvm.test.d.ts.map +1 -0
  195. package/dist/proxmox/probes/lvm.test.js +128 -0
  196. package/dist/proxmox/probes/lvm.test.js.map +1 -0
  197. package/dist/proxmox/probes/lxc-config.d.ts +29 -0
  198. package/dist/proxmox/probes/lxc-config.d.ts.map +1 -0
  199. package/dist/proxmox/probes/lxc-config.js +67 -0
  200. package/dist/proxmox/probes/lxc-config.js.map +1 -0
  201. package/dist/proxmox/probes/lxc-config.test.d.ts +2 -0
  202. package/dist/proxmox/probes/lxc-config.test.d.ts.map +1 -0
  203. package/dist/proxmox/probes/lxc-config.test.js +77 -0
  204. package/dist/proxmox/probes/lxc-config.test.js.map +1 -0
  205. package/dist/proxmox/probes/lxc-list.d.ts +20 -0
  206. package/dist/proxmox/probes/lxc-list.d.ts.map +1 -0
  207. package/dist/proxmox/probes/lxc-list.js +49 -0
  208. package/dist/proxmox/probes/lxc-list.js.map +1 -0
  209. package/dist/proxmox/probes/lxc-list.test.d.ts +2 -0
  210. package/dist/proxmox/probes/lxc-list.test.d.ts.map +1 -0
  211. package/dist/proxmox/probes/lxc-list.test.js +51 -0
  212. package/dist/proxmox/probes/lxc-list.test.js.map +1 -0
  213. package/dist/proxmox/probes/vm-config.d.ts +21 -0
  214. package/dist/proxmox/probes/vm-config.d.ts.map +1 -0
  215. package/dist/proxmox/probes/vm-config.js +58 -0
  216. package/dist/proxmox/probes/vm-config.js.map +1 -0
  217. package/dist/proxmox/probes/vm-config.test.d.ts +2 -0
  218. package/dist/proxmox/probes/vm-config.test.d.ts.map +1 -0
  219. package/dist/proxmox/probes/vm-config.test.js +80 -0
  220. package/dist/proxmox/probes/vm-config.test.js.map +1 -0
  221. package/dist/proxmox/probes/vm-locks.d.ts +16 -0
  222. package/dist/proxmox/probes/vm-locks.d.ts.map +1 -0
  223. package/dist/proxmox/probes/vm-locks.js +35 -0
  224. package/dist/proxmox/probes/vm-locks.js.map +1 -0
  225. package/dist/proxmox/probes/vm-locks.test.d.ts +2 -0
  226. package/dist/proxmox/probes/vm-locks.test.d.ts.map +1 -0
  227. package/dist/proxmox/probes/vm-locks.test.js +54 -0
  228. package/dist/proxmox/probes/vm-locks.test.js.map +1 -0
  229. package/dist/redis/index.d.ts +3 -0
  230. package/dist/redis/index.d.ts.map +1 -0
  231. package/dist/redis/index.js +13 -0
  232. package/dist/redis/index.js.map +1 -0
  233. package/dist/redis/manifest.d.ts +3 -0
  234. package/dist/redis/manifest.d.ts.map +1 -0
  235. package/dist/redis/manifest.js +51 -0
  236. package/dist/redis/manifest.js.map +1 -0
  237. package/dist/redis/probes/info.d.ts +15 -0
  238. package/dist/redis/probes/info.d.ts.map +1 -0
  239. package/dist/redis/probes/info.js +32 -0
  240. package/dist/redis/probes/info.js.map +1 -0
  241. package/dist/redis/probes/info.test.d.ts +2 -0
  242. package/dist/redis/probes/info.test.d.ts.map +1 -0
  243. package/dist/redis/probes/info.test.js +64 -0
  244. package/dist/redis/probes/info.test.js.map +1 -0
  245. package/dist/redis/probes/keys-count.d.ts +13 -0
  246. package/dist/redis/probes/keys-count.d.ts.map +1 -0
  247. package/dist/redis/probes/keys-count.js +24 -0
  248. package/dist/redis/probes/keys-count.js.map +1 -0
  249. package/dist/redis/probes/keys-count.test.d.ts +2 -0
  250. package/dist/redis/probes/keys-count.test.d.ts.map +1 -0
  251. package/dist/redis/probes/keys-count.test.js +37 -0
  252. package/dist/redis/probes/keys-count.test.js.map +1 -0
  253. package/dist/redis/probes/memory-usage.d.ts +16 -0
  254. package/dist/redis/probes/memory-usage.d.ts.map +1 -0
  255. package/dist/redis/probes/memory-usage.js +31 -0
  256. package/dist/redis/probes/memory-usage.js.map +1 -0
  257. package/dist/redis/probes/memory-usage.test.d.ts +2 -0
  258. package/dist/redis/probes/memory-usage.test.d.ts.map +1 -0
  259. package/dist/redis/probes/memory-usage.test.js +48 -0
  260. package/dist/redis/probes/memory-usage.test.js.map +1 -0
  261. package/dist/runbooks/nutanix.d.ts +3 -0
  262. package/dist/runbooks/nutanix.d.ts.map +1 -0
  263. package/dist/runbooks/nutanix.js +619 -0
  264. package/dist/runbooks/nutanix.js.map +1 -0
  265. package/dist/runbooks/nutanix.test.d.ts +2 -0
  266. package/dist/runbooks/nutanix.test.d.ts.map +1 -0
  267. package/dist/runbooks/nutanix.test.js +971 -0
  268. package/dist/runbooks/nutanix.test.js.map +1 -0
  269. package/dist/runbooks/proxmox.d.ts +3 -0
  270. package/dist/runbooks/proxmox.d.ts.map +1 -0
  271. package/dist/runbooks/proxmox.js +451 -0
  272. package/dist/runbooks/proxmox.js.map +1 -0
  273. package/dist/runbooks/proxmox.test.d.ts +2 -0
  274. package/dist/runbooks/proxmox.test.d.ts.map +1 -0
  275. package/dist/runbooks/proxmox.test.js +700 -0
  276. package/dist/runbooks/proxmox.test.js.map +1 -0
  277. package/dist/signatures.d.ts +2 -0
  278. package/dist/signatures.d.ts.map +1 -0
  279. package/dist/signatures.js +2 -0
  280. package/dist/signatures.js.map +1 -0
  281. package/dist/types.d.ts +53 -0
  282. package/dist/types.d.ts.map +1 -1
  283. package/dist/validation.d.ts +6 -1
  284. package/dist/validation.d.ts.map +1 -1
  285. package/dist/validation.js +10 -1
  286. package/dist/validation.js.map +1 -1
  287. package/package.json +1 -1
  288. package/src/index.ts +60 -6
  289. package/src/integrations/citrix.test.ts +592 -0
  290. package/src/integrations/citrix.ts +557 -0
  291. package/src/integrations/graph.test.ts +478 -0
  292. package/src/integrations/graph.ts +413 -0
  293. package/src/integrations/httpbin.ts +72 -0
  294. package/src/integrations/nutanix.test.ts +1508 -0
  295. package/src/integrations/nutanix.ts +1460 -0
  296. package/src/integrations/proxmox.test.ts +1020 -0
  297. package/src/integrations/proxmox.ts +989 -0
  298. package/src/integrations/servicenow.test.ts +314 -0
  299. package/src/integrations/servicenow.ts +285 -0
  300. package/src/integrations/splunk.test.ts +440 -0
  301. package/src/integrations/splunk.ts +356 -0
  302. package/src/mysql/index.ts +14 -0
  303. package/src/mysql/manifest.ts +70 -0
  304. package/src/mysql/probes/databases-list.test.ts +62 -0
  305. package/src/mysql/probes/databases-list.ts +45 -0
  306. package/src/mysql/probes/processlist.test.ts +47 -0
  307. package/src/mysql/probes/processlist.ts +55 -0
  308. package/src/mysql/probes/status.test.ts +50 -0
  309. package/src/mysql/probes/status.ts +56 -0
  310. package/src/nginx/index.ts +14 -0
  311. package/src/nginx/manifest.ts +69 -0
  312. package/src/nginx/probes/access-log-tail.test.ts +51 -0
  313. package/src/nginx/probes/access-log-tail.ts +23 -0
  314. package/src/nginx/probes/config-test.test.ts +47 -0
  315. package/src/nginx/probes/config-test.ts +24 -0
  316. package/src/nginx/probes/error-log-tail.test.ts +44 -0
  317. package/src/nginx/probes/error-log-tail.ts +23 -0
  318. package/src/postgres/index.ts +14 -0
  319. package/src/postgres/manifest.ts +91 -0
  320. package/src/postgres/probes/connections-active.test.ts +42 -0
  321. package/src/postgres/probes/connections-active.ts +55 -0
  322. package/src/postgres/probes/databases-list.test.ts +57 -0
  323. package/src/postgres/probes/databases-list.ts +49 -0
  324. package/src/postgres/probes/query-slow.test.ts +37 -0
  325. package/src/postgres/probes/query-slow.ts +55 -0
  326. package/src/proxmox/index.ts +24 -0
  327. package/src/proxmox/manifest.ts +76 -0
  328. package/src/proxmox/probes/ceph-status.test.ts +126 -0
  329. package/src/proxmox/probes/ceph-status.ts +116 -0
  330. package/src/proxmox/probes/cluster-config.test.ts +118 -0
  331. package/src/proxmox/probes/cluster-config.ts +97 -0
  332. package/src/proxmox/probes/ha-status.test.ts +76 -0
  333. package/src/proxmox/probes/ha-status.ts +56 -0
  334. package/src/proxmox/probes/lvm.test.ts +140 -0
  335. package/src/proxmox/probes/lvm.ts +121 -0
  336. package/src/proxmox/probes/lxc-config.test.ts +89 -0
  337. package/src/proxmox/probes/lxc-config.ts +90 -0
  338. package/src/proxmox/probes/lxc-list.test.ts +60 -0
  339. package/src/proxmox/probes/lxc-list.ts +67 -0
  340. package/src/proxmox/probes/vm-config.test.ts +93 -0
  341. package/src/proxmox/probes/vm-config.ts +77 -0
  342. package/src/proxmox/probes/vm-locks.test.ts +63 -0
  343. package/src/proxmox/probes/vm-locks.ts +49 -0
  344. package/src/redis/index.ts +14 -0
  345. package/src/redis/manifest.ts +52 -0
  346. package/src/redis/probes/info.test.ts +73 -0
  347. package/src/redis/probes/info.ts +46 -0
  348. package/src/redis/probes/keys-count.test.ts +44 -0
  349. package/src/redis/probes/keys-count.ts +38 -0
  350. package/src/redis/probes/memory-usage.test.ts +54 -0
  351. package/src/redis/probes/memory-usage.ts +46 -0
  352. package/src/runbooks/nutanix.test.ts +1138 -0
  353. package/src/runbooks/nutanix.ts +941 -0
  354. package/src/runbooks/proxmox.test.ts +838 -0
  355. package/src/runbooks/proxmox.ts +626 -0
  356. package/src/signatures.ts +1 -0
  357. package/src/types.ts +62 -0
  358. package/src/validation.ts +21 -1
  359. package/tsconfig.tsbuildinfo +1 -0
@@ -0,0 +1,37 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import type { ExecFn } from '../../types.js';
3
+ import type { QuerySlowResult } from './query-slow.js';
4
+ import { parseQuerySlow, querySlow } from './query-slow.js';
5
+
6
+ const SAMPLE_OUTPUT =
7
+ '9876\tmyapp\tappuser\t5432\tactive\tSELECT * FROM large_table WHERE expensive_join';
8
+
9
+ describe('parseQuerySlow', () => {
10
+ it('parses slow queries', () => {
11
+ const result = parseQuerySlow(SAMPLE_OUTPUT, 1000);
12
+ expect(result.count).toBe(1);
13
+ expect(result.thresholdMs).toBe(1000);
14
+ expect(result.queries[0]?.pid).toBe(9876);
15
+ expect(result.queries[0]?.query).toContain('expensive_join');
16
+ });
17
+
18
+ it('handles no slow queries', () => {
19
+ const result = parseQuerySlow('', 1000);
20
+ expect(result.count).toBe(0);
21
+ expect(result.queries).toEqual([]);
22
+ });
23
+ });
24
+
25
+ describe('querySlow handler', () => {
26
+ it('calls psql with threshold param', async () => {
27
+ const mockExec: ExecFn = async (cmd, args) => {
28
+ expect(cmd).toBe('psql');
29
+ const query = args[args.length - 1];
30
+ expect(query).toContain('2000');
31
+ return SAMPLE_OUTPUT;
32
+ };
33
+
34
+ const result = (await querySlow({ thresholdMs: 2000 }, mockExec)) as QuerySlowResult;
35
+ expect(result.thresholdMs).toBe(2000);
36
+ });
37
+ });
@@ -0,0 +1,55 @@
1
+ import type { ProbeHandler } from '../../types.js';
2
+
3
+ export interface SlowQueryInfo {
4
+ pid: number;
5
+ database: string;
6
+ user: string;
7
+ durationMs: number;
8
+ state: string;
9
+ query: string;
10
+ }
11
+
12
+ export interface QuerySlowResult {
13
+ queries: SlowQueryInfo[];
14
+ count: number;
15
+ thresholdMs: number;
16
+ }
17
+
18
+ export const querySlow: ProbeHandler = async (params, exec) => {
19
+ const host = (params?.host as string) ?? 'localhost';
20
+ const port = String((params?.port as number) ?? 5432);
21
+ const user = (params?.user as string) ?? 'postgres';
22
+ const thresholdMs = (params?.thresholdMs as number) ?? 1000;
23
+
24
+ const stdout = await exec('psql', [
25
+ '-h',
26
+ host,
27
+ '-p',
28
+ port,
29
+ '-U',
30
+ user,
31
+ '-t',
32
+ '-A',
33
+ '-F',
34
+ '\t',
35
+ '-c',
36
+ `SELECT pid, datname, usename, EXTRACT(EPOCH FROM (now() - query_start))::int * 1000, state, LEFT(query, 300) FROM pg_stat_activity WHERE state = 'active' AND pid <> pg_backend_pid() AND EXTRACT(EPOCH FROM (now() - query_start)) * 1000 > ${thresholdMs} ORDER BY query_start ASC`,
37
+ ]);
38
+ return parseQuerySlow(stdout, thresholdMs);
39
+ };
40
+
41
+ export function parseQuerySlow(stdout: string, thresholdMs: number): QuerySlowResult {
42
+ const lines = stdout.trim().split('\n').filter(Boolean);
43
+ const queries: SlowQueryInfo[] = lines.map((line) => {
44
+ const parts = line.split('\t');
45
+ return {
46
+ pid: Number(parts[0]) || 0,
47
+ database: parts[1] ?? '',
48
+ user: parts[2] ?? '',
49
+ durationMs: Number(parts[3]) || 0,
50
+ state: parts[4] ?? '',
51
+ query: parts[5] ?? '',
52
+ };
53
+ });
54
+ return { queries, count: queries.length, thresholdMs };
55
+ }
@@ -0,0 +1,24 @@
1
+ import type { Pack } from '../types.js';
2
+ import { proxmoxAgentManifest } from './manifest.js';
3
+ import { cephStatus } from './probes/ceph-status.js';
4
+ import { clusterConfig } from './probes/cluster-config.js';
5
+ import { haStatus } from './probes/ha-status.js';
6
+ import { lvm } from './probes/lvm.js';
7
+ import { lxcConfig } from './probes/lxc-config.js';
8
+ import { lxcList } from './probes/lxc-list.js';
9
+ import { vmConfig } from './probes/vm-config.js';
10
+ import { vmLocks } from './probes/vm-locks.js';
11
+
12
+ export const proxmoxAgentPack: Pack = {
13
+ manifest: proxmoxAgentManifest,
14
+ handlers: {
15
+ 'proxmox-node.local.vm.config': vmConfig,
16
+ 'proxmox-node.local.ha.status': haStatus,
17
+ 'proxmox-node.local.lvm': lvm,
18
+ 'proxmox-node.local.ceph.status': cephStatus,
19
+ 'proxmox-node.local.lxc.config': lxcConfig,
20
+ 'proxmox-node.local.lxc.list': lxcList,
21
+ 'proxmox-node.local.cluster.config': clusterConfig,
22
+ 'proxmox-node.local.vm.locks': vmLocks,
23
+ },
24
+ };
@@ -0,0 +1,76 @@
1
+ import type { PackManifest } from '@sonde/shared';
2
+
3
+ export const proxmoxAgentManifest: PackManifest = {
4
+ name: 'proxmox-node',
5
+ version: '0.1.0',
6
+ description: 'Proxmox VE node-local probes — VM/LXC config, HA status, LVM, Ceph, cluster',
7
+ requires: {
8
+ groups: [],
9
+ files: ['/etc/pve/'],
10
+ commands: ['qm', 'pct', 'ha-manager', 'pvesh', 'lvs', 'vgs', 'pvs', 'pvecm'],
11
+ },
12
+ probes: [
13
+ {
14
+ name: 'local.vm.config',
15
+ description: 'QEMU VM configuration via qm config',
16
+ capability: 'observe',
17
+ params: {
18
+ vmid: { type: 'number', description: 'VM ID', required: true },
19
+ },
20
+ timeout: 10_000,
21
+ },
22
+ {
23
+ name: 'local.ha.status',
24
+ description: 'HA manager resource states',
25
+ capability: 'observe',
26
+ timeout: 10_000,
27
+ },
28
+ {
29
+ name: 'local.lvm',
30
+ description: 'LVM topology — logical volumes, volume groups, physical volumes',
31
+ capability: 'observe',
32
+ timeout: 15_000,
33
+ },
34
+ {
35
+ name: 'local.ceph.status',
36
+ description: 'Ceph cluster health, OSD status, and PG states',
37
+ capability: 'observe',
38
+ timeout: 15_000,
39
+ },
40
+ {
41
+ name: 'local.lxc.config',
42
+ description: 'LXC container configuration via pct config',
43
+ capability: 'observe',
44
+ params: {
45
+ vmid: { type: 'number', description: 'Container VMID', required: true },
46
+ },
47
+ timeout: 10_000,
48
+ },
49
+ {
50
+ name: 'local.lxc.list',
51
+ description: 'List all LXC containers on this node',
52
+ capability: 'observe',
53
+ timeout: 10_000,
54
+ },
55
+ {
56
+ name: 'local.cluster.config',
57
+ description: 'Cluster membership, quorum, and vote status via pvecm',
58
+ capability: 'observe',
59
+ timeout: 10_000,
60
+ },
61
+ {
62
+ name: 'local.vm.locks',
63
+ description: 'Check for locked QEMU VMs via /run/lock/qemu-server/',
64
+ capability: 'observe',
65
+ timeout: 10_000,
66
+ },
67
+ ],
68
+ runbook: {
69
+ category: 'proxmox',
70
+ probes: ['local.ha.status', 'local.lvm', 'local.cluster.config'],
71
+ parallel: true,
72
+ },
73
+ detect: {
74
+ commands: ['qm', 'pct'],
75
+ },
76
+ };
@@ -0,0 +1,126 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import type { ExecFn } from '../../types.js';
3
+ import type { CephStatusResult } from './ceph-status.js';
4
+ import { cephStatus, parseOsdTree } from './ceph-status.js';
5
+
6
+ const CEPH_STATUS_JSON = JSON.stringify({
7
+ health: { status: 'HEALTH_OK' },
8
+ osdmap: { num_osds: 6, num_up_osds: 6, num_in_osds: 6 },
9
+ pgmap: {
10
+ pgs_by_state: [{ state_name: 'active+clean', count: 256 }],
11
+ bytes_total: 6000000000000,
12
+ bytes_used: 2000000000000,
13
+ bytes_avail: 4000000000000,
14
+ },
15
+ });
16
+
17
+ const OSD_TREE_JSON = JSON.stringify({
18
+ nodes: [
19
+ { id: -1, name: 'default', type: 'root' },
20
+ { id: -2, name: 'pve01', type: 'host' },
21
+ {
22
+ id: 0,
23
+ name: 'osd.0',
24
+ type: 'osd',
25
+ status: 'up',
26
+ crush_weight: 1.0,
27
+ reweight: 1.0,
28
+ host: 'pve01',
29
+ },
30
+ {
31
+ id: 1,
32
+ name: 'osd.1',
33
+ type: 'osd',
34
+ status: 'up',
35
+ crush_weight: 1.0,
36
+ reweight: 1.0,
37
+ host: 'pve01',
38
+ },
39
+ {
40
+ id: 2,
41
+ name: 'osd.2',
42
+ type: 'osd',
43
+ status: 'down',
44
+ crush_weight: 1.0,
45
+ reweight: 0,
46
+ host: 'pve02',
47
+ },
48
+ ],
49
+ });
50
+
51
+ describe('cephStatus handler', () => {
52
+ it('returns health, OSD counts, and PG states', async () => {
53
+ let callCount = 0;
54
+ const mockExec: ExecFn = async (cmd, args) => {
55
+ callCount++;
56
+ expect(cmd).toBe('ceph');
57
+ if (callCount === 1) {
58
+ expect(args).toEqual(['status', '--format', 'json']);
59
+ return CEPH_STATUS_JSON;
60
+ }
61
+ expect(args).toEqual(['osd', 'tree', '--format', 'json']);
62
+ return OSD_TREE_JSON;
63
+ };
64
+
65
+ const result = (await cephStatus(undefined, mockExec)) as CephStatusResult;
66
+ expect(result.available).toBe(true);
67
+ expect(result.health).toBe('HEALTH_OK');
68
+ expect(result.osdCount).toBe(6);
69
+ expect(result.osdUp).toBe(6);
70
+ expect(result.osdIn).toBe(6);
71
+ expect(result.pgStates).toHaveLength(1);
72
+ expect(result.pgStates[0]).toEqual({ state: 'active+clean', count: 256 });
73
+ expect(result.usage.total).toBe(6000000000000);
74
+ expect(result.osds).toHaveLength(3);
75
+ expect(result.warnings).toHaveLength(0);
76
+ });
77
+
78
+ it('flags degraded health', async () => {
79
+ const degraded = JSON.stringify({
80
+ health: { status: 'HEALTH_WARN' },
81
+ osdmap: { num_osds: 6, num_up_osds: 4, num_in_osds: 6 },
82
+ pgmap: { bytes_total: 0, bytes_used: 0, bytes_avail: 0 },
83
+ });
84
+
85
+ const mockExec: ExecFn = async (cmd, args) => {
86
+ if (args[0] === 'status') return degraded;
87
+ return '{"nodes":[]}';
88
+ };
89
+
90
+ const result = (await cephStatus(undefined, mockExec)) as CephStatusResult;
91
+ expect(result.warnings.some((w) => w.includes('HEALTH_WARN'))).toBe(true);
92
+ expect(result.warnings.some((w) => w.includes('2 OSD(s) down'))).toBe(true);
93
+ });
94
+
95
+ it('handles missing ceph binary gracefully', async () => {
96
+ const mockExec: ExecFn = async () => {
97
+ throw new Error('command not found: ceph');
98
+ };
99
+
100
+ const result = (await cephStatus(undefined, mockExec)) as { available: boolean };
101
+ expect(result.available).toBe(false);
102
+ });
103
+ });
104
+
105
+ describe('parseOsdTree', () => {
106
+ it('extracts only OSD nodes from the tree', () => {
107
+ const osds = parseOsdTree(OSD_TREE_JSON);
108
+ expect(osds).toHaveLength(3);
109
+ expect(osds[0]).toEqual({
110
+ id: 0,
111
+ name: 'osd.0',
112
+ type: 'osd',
113
+ status: 'up',
114
+ crush_weight: 1.0,
115
+ reweight: 1.0,
116
+ host: 'pve01',
117
+ });
118
+ expect(osds[2]?.status).toBe('down');
119
+ });
120
+
121
+ it('filters out non-OSD nodes', () => {
122
+ const osds = parseOsdTree(OSD_TREE_JSON);
123
+ const types = osds.map((o) => o.type);
124
+ expect(types.every((t) => t === 'osd')).toBe(true);
125
+ });
126
+ });
@@ -0,0 +1,116 @@
1
+ import type { ProbeHandler } from '../../types.js';
2
+
3
+ export interface OsdNode {
4
+ id: number;
5
+ name: string;
6
+ type: string;
7
+ status: string;
8
+ crush_weight: number;
9
+ reweight: number;
10
+ host: string;
11
+ }
12
+
13
+ export interface CephStatusResult {
14
+ available: boolean;
15
+ health: string;
16
+ osdCount: number;
17
+ osdUp: number;
18
+ osdIn: number;
19
+ pgStates: Array<{ state: string; count: number }>;
20
+ usage: { total: number; used: number; avail: number };
21
+ osds: OsdNode[];
22
+ warnings: string[];
23
+ }
24
+
25
+ /**
26
+ * Runs `ceph status --format json` and `ceph osd tree --format json`.
27
+ * Gracefully handles missing ceph binary.
28
+ */
29
+ export const cephStatus: ProbeHandler = async (_params, exec) => {
30
+ let statusOut: string;
31
+ try {
32
+ statusOut = await exec('ceph', ['status', '--format', 'json']);
33
+ } catch {
34
+ return {
35
+ available: false,
36
+ health: null,
37
+ warnings: ['Ceph is not installed or not accessible on this node'],
38
+ };
39
+ }
40
+
41
+ return parseCephStatus(statusOut, exec);
42
+ };
43
+
44
+ export async function parseCephStatus(
45
+ statusOut: string,
46
+ exec?: (cmd: string, args: string[]) => Promise<string>,
47
+ ): Promise<CephStatusResult> {
48
+ const data = JSON.parse(statusOut);
49
+ const warnings: string[] = [];
50
+
51
+ const health = data.health?.status ?? 'unknown';
52
+ const osdmap = data.osdmap ?? {};
53
+ const osdCount = osdmap.num_osds ?? 0;
54
+ const osdUp = osdmap.num_up_osds ?? 0;
55
+ const osdIn = osdmap.num_in_osds ?? 0;
56
+
57
+ if (health !== 'HEALTH_OK') {
58
+ warnings.push(`Ceph health: ${health}`);
59
+ }
60
+ if (osdCount > 0 && osdUp < osdCount) {
61
+ warnings.push(`${osdCount - osdUp} OSD(s) down`);
62
+ }
63
+
64
+ const pgmap = data.pgmap ?? {};
65
+ const pgStates: Array<{ state: string; count: number }> = (pgmap.pgs_by_state ?? []).map(
66
+ (p: { state_name?: string; count?: number }) => ({
67
+ state: p.state_name ?? '',
68
+ count: p.count ?? 0,
69
+ }),
70
+ );
71
+
72
+ const usage = {
73
+ total: pgmap.bytes_total ?? 0,
74
+ used: pgmap.bytes_used ?? 0,
75
+ avail: pgmap.bytes_avail ?? 0,
76
+ };
77
+
78
+ // OSD tree for placement info
79
+ let osds: OsdNode[] = [];
80
+ if (exec) {
81
+ try {
82
+ const treeOut = await exec('ceph', ['osd', 'tree', '--format', 'json']);
83
+ osds = parseOsdTree(treeOut);
84
+ } catch {
85
+ // OSD tree is best-effort
86
+ }
87
+ }
88
+
89
+ return { available: true, health, osdCount, osdUp, osdIn, pgStates, usage, osds, warnings };
90
+ }
91
+
92
+ export function parseOsdTree(stdout: string): OsdNode[] {
93
+ const data = JSON.parse(stdout);
94
+ const nodes = data.nodes ?? [];
95
+ return nodes
96
+ .filter((n: { type?: string }) => n.type === 'osd')
97
+ .map(
98
+ (n: {
99
+ id?: number;
100
+ name?: string;
101
+ type?: string;
102
+ status?: string;
103
+ crush_weight?: number;
104
+ reweight?: number;
105
+ host?: string;
106
+ }) => ({
107
+ id: n.id ?? 0,
108
+ name: n.name ?? '',
109
+ type: n.type ?? 'osd',
110
+ status: n.status ?? 'unknown',
111
+ crush_weight: n.crush_weight ?? 0,
112
+ reweight: n.reweight ?? 0,
113
+ host: n.host ?? '',
114
+ }),
115
+ );
116
+ }
@@ -0,0 +1,118 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import type { ExecFn } from '../../types.js';
3
+ import type { ClusterConfigResult } from './cluster-config.js';
4
+ import { clusterConfig, parseClusterConfig } from './cluster-config.js';
5
+
6
+ const SAMPLE_OUTPUT = `Cluster information
7
+ ~~~~~~~~~~~~~~~~~~
8
+ Name: mycluster
9
+ Config Version: 3
10
+ Transport: knet
11
+ Secure auth: on
12
+
13
+ Quorum information
14
+ ~~~~~~~~~~~~~~~~~~
15
+ Date: Mon Feb 17 10:00:00 2026
16
+ Quorum provider: corosync_votequorum
17
+ Nodes: 3
18
+ Node ID: 0x00000001
19
+ Ring ID: 1.123
20
+ Quorate: Yes
21
+
22
+ Votequorum information
23
+ ~~~~~~~~~~~~~~~~~~~~~~
24
+ Expected votes: 3
25
+ Highest expected: 3
26
+ Total votes: 3
27
+ Quorum: 2
28
+ Flags: Quorate
29
+
30
+ Membership information
31
+ ~~~~~~~~~~~~~~~~~~~~~~
32
+ Nodeid Votes Name
33
+ 1 1 pve01 (local)
34
+ 2 1 pve02
35
+ 3 1 pve03`;
36
+
37
+ describe('parseClusterConfig', () => {
38
+ it('extracts cluster name', () => {
39
+ const result = parseClusterConfig(SAMPLE_OUTPUT);
40
+ expect(result.clusterName).toBe('mycluster');
41
+ });
42
+
43
+ it('detects quorate status', () => {
44
+ const result = parseClusterConfig(SAMPLE_OUTPUT);
45
+ expect(result.quorate).toBe(true);
46
+ });
47
+
48
+ it('extracts vote counts', () => {
49
+ const result = parseClusterConfig(SAMPLE_OUTPUT);
50
+ expect(result.totalVotes).toBe(3);
51
+ expect(result.expectedVotes).toBe(3);
52
+ });
53
+
54
+ it('parses membership nodes', () => {
55
+ const result = parseClusterConfig(SAMPLE_OUTPUT);
56
+ expect(result.nodes).toHaveLength(3);
57
+ expect(result.nodes[0]).toEqual({
58
+ nodeId: '1',
59
+ name: 'pve01',
60
+ votes: 1,
61
+ local: true,
62
+ });
63
+ expect(result.nodes[1]).toEqual({
64
+ nodeId: '2',
65
+ name: 'pve02',
66
+ votes: 1,
67
+ local: false,
68
+ });
69
+ });
70
+
71
+ it('no warnings when quorate', () => {
72
+ const result = parseClusterConfig(SAMPLE_OUTPUT);
73
+ expect(result.warnings).toHaveLength(0);
74
+ });
75
+
76
+ it('warns when not quorate', () => {
77
+ const notQuorate = SAMPLE_OUTPUT.replace('Quorate: Yes', 'Quorate: No');
78
+ const result = parseClusterConfig(notQuorate);
79
+ expect(result.warnings).toContain('Cluster is not quorate');
80
+ });
81
+
82
+ it('handles single-node output', () => {
83
+ const singleNode = `Cluster information
84
+ ~~~~~~~~~~~~~~~~~~
85
+ Name: standalone
86
+
87
+ Quorum information
88
+ ~~~~~~~~~~~~~~~~~~
89
+ Quorate: Yes
90
+
91
+ Votequorum information
92
+ ~~~~~~~~~~~~~~~~~~~~~~
93
+ Expected votes: 1
94
+ Total votes: 1
95
+
96
+ Membership information
97
+ ~~~~~~~~~~~~~~~~~~~~~~
98
+ Nodeid Votes Name
99
+ 1 1 pve01 (local)`;
100
+ const result = parseClusterConfig(singleNode);
101
+ expect(result.nodes).toHaveLength(1);
102
+ expect(result.nodes[0]?.local).toBe(true);
103
+ });
104
+ });
105
+
106
+ describe('clusterConfig handler', () => {
107
+ it('calls pvecm status and returns parsed result', async () => {
108
+ const mockExec: ExecFn = async (cmd, args) => {
109
+ expect(cmd).toBe('pvecm');
110
+ expect(args).toEqual(['status']);
111
+ return SAMPLE_OUTPUT;
112
+ };
113
+
114
+ const result = (await clusterConfig(undefined, mockExec)) as ClusterConfigResult;
115
+ expect(result.clusterName).toBe('mycluster');
116
+ expect(result.nodes).toHaveLength(3);
117
+ });
118
+ });
@@ -0,0 +1,97 @@
1
+ import type { ProbeHandler } from '../../types.js';
2
+
3
+ export interface ClusterNode {
4
+ nodeId: string;
5
+ name: string;
6
+ votes: number;
7
+ local: boolean;
8
+ }
9
+
10
+ export interface ClusterConfigResult {
11
+ clusterName: string;
12
+ quorate: boolean;
13
+ totalVotes: number;
14
+ expectedVotes: number;
15
+ nodes: ClusterNode[];
16
+ warnings: string[];
17
+ }
18
+
19
+ /**
20
+ * Runs `pvecm status` and parses cluster membership.
21
+ * Output contains sections like:
22
+ * Cluster information
23
+ * ~~~~~~~~~~~~~~~~~~
24
+ * Name: mycluster
25
+ * ...
26
+ * Membership information
27
+ * ~~~~~~~~~~~~~~~~~~~~~~
28
+ * Nodeid Votes Name
29
+ * 1 1 pve01 (local)
30
+ * 2 1 pve02
31
+ */
32
+ export const clusterConfig: ProbeHandler = async (_params, exec) => {
33
+ const stdout = await exec('pvecm', ['status']);
34
+ return parseClusterConfig(stdout);
35
+ };
36
+
37
+ export function parseClusterConfig(stdout: string): ClusterConfigResult {
38
+ const lines = stdout.trim().split('\n');
39
+ const warnings: string[] = [];
40
+
41
+ let clusterName = '';
42
+ let quorate = false;
43
+ let totalVotes = 0;
44
+ let expectedVotes = 0;
45
+ const nodes: ClusterNode[] = [];
46
+
47
+ let inMembership = false;
48
+ let membershipHeaderSeen = false;
49
+
50
+ for (const line of lines) {
51
+ const trimmed = line.trim();
52
+
53
+ // Key: Value pairs in cluster info section
54
+ if (trimmed.startsWith('Name:')) {
55
+ clusterName = trimmed.slice(5).trim();
56
+ }
57
+ if (trimmed.startsWith('Quorate:')) {
58
+ quorate = trimmed.toLowerCase().includes('yes');
59
+ }
60
+ if (trimmed.startsWith('Total votes:')) {
61
+ totalVotes = Number.parseInt(trimmed.slice('Total votes:'.length).trim(), 10) || 0;
62
+ }
63
+ if (trimmed.startsWith('Expected votes:')) {
64
+ expectedVotes = Number.parseInt(trimmed.slice('Expected votes:'.length).trim(), 10) || 0;
65
+ }
66
+
67
+ // Membership section detection
68
+ if (trimmed.startsWith('Membership information') || trimmed.startsWith('Nodeid')) {
69
+ inMembership = true;
70
+ if (trimmed.startsWith('Nodeid')) membershipHeaderSeen = true;
71
+ continue;
72
+ }
73
+ if (trimmed.startsWith('~~~')) continue;
74
+
75
+ // Parse node lines: "1 1 pve01 (local)"
76
+ if (inMembership && membershipHeaderSeen && trimmed) {
77
+ const parts = trimmed.split(/\s+/);
78
+ if (parts.length >= 3) {
79
+ const nodeId = parts[0] ?? '';
80
+ const votes = Number.parseInt(parts[1] ?? '', 10);
81
+ if (Number.isNaN(votes)) continue;
82
+
83
+ const nameAndFlags = parts.slice(2).join(' ');
84
+ const local = nameAndFlags.includes('(local)');
85
+ const name = nameAndFlags.replace('(local)', '').trim();
86
+
87
+ nodes.push({ nodeId, name, votes, local });
88
+ }
89
+ }
90
+ }
91
+
92
+ if (!quorate) {
93
+ warnings.push('Cluster is not quorate');
94
+ }
95
+
96
+ return { clusterName, quorate, totalVotes, expectedVotes, nodes, warnings };
97
+ }