@harperfast/harper 5.2.0-beta.4 → 5.2.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 (320) hide show
  1. package/bin/backup.ts +169 -0
  2. package/bin/cliOperations.ts +92 -73
  3. package/bin/copyDb.ts +21 -4
  4. package/bin/harper.ts +43 -55
  5. package/bin/help.ts +216 -0
  6. package/components/Application.ts +236 -46
  7. package/components/ApplicationScope.ts +26 -0
  8. package/components/EntryHandler.ts +410 -105
  9. package/components/RuntimeModuleTracker.ts +189 -0
  10. package/components/Scope.ts +120 -30
  11. package/components/componentLoader.ts +177 -25
  12. package/components/deployLifecycle.ts +119 -33
  13. package/components/mcp/toolRegistry.ts +10 -0
  14. package/components/mcp/tools/application.ts +12 -5
  15. package/components/mcp/tools/operations.ts +9 -0
  16. package/components/mcp/tools/schemas/operationDescriptions.ts +3 -0
  17. package/components/mcp/tools/schemas/operations.ts +9 -0
  18. package/components/operations.js +5 -6
  19. package/components/operationsValidation.js +32 -2
  20. package/components/scopeMount.ts +150 -0
  21. package/config/configUtils.ts +12 -9
  22. package/config-root.schema.json +14 -0
  23. package/dataLayer/backupManifest.ts +102 -0
  24. package/dataLayer/blobBackup.ts +286 -0
  25. package/dataLayer/harperBridge/ResourceBridge.ts +69 -5
  26. package/dataLayer/hdbInfoController.ts +8 -0
  27. package/dataLayer/restoreMarker.ts +276 -0
  28. package/dataLayer/rocksdbBackup.ts +1100 -0
  29. package/dataLayer/schemaDescribe.ts +2 -1
  30. package/dist/bin/backup.d.ts +9 -0
  31. package/dist/bin/backup.js +192 -0
  32. package/dist/bin/backup.js.map +1 -0
  33. package/dist/bin/cliOperations.d.ts +13 -0
  34. package/dist/bin/cliOperations.js +89 -70
  35. package/dist/bin/cliOperations.js.map +1 -1
  36. package/dist/bin/copyDb.js +13 -2
  37. package/dist/bin/copyDb.js.map +1 -1
  38. package/dist/bin/harper.d.ts +13 -0
  39. package/dist/bin/harper.js +45 -56
  40. package/dist/bin/harper.js.map +1 -1
  41. package/dist/bin/help.d.ts +8 -0
  42. package/dist/bin/help.js +192 -0
  43. package/dist/bin/help.js.map +1 -0
  44. package/dist/components/Application.d.ts +16 -1
  45. package/dist/components/Application.js +210 -38
  46. package/dist/components/Application.js.map +1 -1
  47. package/dist/components/ApplicationScope.d.ts +7 -0
  48. package/dist/components/ApplicationScope.js +22 -0
  49. package/dist/components/ApplicationScope.js.map +1 -1
  50. package/dist/components/EntryHandler.d.ts +4 -4
  51. package/dist/components/EntryHandler.js +386 -95
  52. package/dist/components/EntryHandler.js.map +1 -1
  53. package/dist/components/RuntimeModuleTracker.d.ts +11 -0
  54. package/dist/components/RuntimeModuleTracker.js +189 -0
  55. package/dist/components/RuntimeModuleTracker.js.map +1 -0
  56. package/dist/components/Scope.d.ts +38 -1
  57. package/dist/components/Scope.js +117 -28
  58. package/dist/components/Scope.js.map +1 -1
  59. package/dist/components/componentLoader.d.ts +2 -6
  60. package/dist/components/componentLoader.js +160 -18
  61. package/dist/components/componentLoader.js.map +1 -1
  62. package/dist/components/deployLifecycle.d.ts +6 -2
  63. package/dist/components/deployLifecycle.js +109 -31
  64. package/dist/components/deployLifecycle.js.map +1 -1
  65. package/dist/components/mcp/toolRegistry.d.ts +7 -0
  66. package/dist/components/mcp/toolRegistry.js +10 -0
  67. package/dist/components/mcp/toolRegistry.js.map +1 -1
  68. package/dist/components/mcp/tools/application.js +11 -5
  69. package/dist/components/mcp/tools/application.js.map +1 -1
  70. package/dist/components/mcp/tools/operations.js +9 -0
  71. package/dist/components/mcp/tools/operations.js.map +1 -1
  72. package/dist/components/mcp/tools/schemas/operationDescriptions.js +2 -0
  73. package/dist/components/mcp/tools/schemas/operationDescriptions.js.map +1 -1
  74. package/dist/components/mcp/tools/schemas/operations.js +9 -0
  75. package/dist/components/mcp/tools/schemas/operations.js.map +1 -1
  76. package/dist/components/operations.js +6 -6
  77. package/dist/components/operations.js.map +1 -1
  78. package/dist/components/operationsValidation.js +34 -2
  79. package/dist/components/operationsValidation.js.map +1 -1
  80. package/dist/components/scopeMount.d.ts +86 -0
  81. package/dist/components/scopeMount.js +131 -0
  82. package/dist/components/scopeMount.js.map +1 -0
  83. package/dist/config/configUtils.js +13 -9
  84. package/dist/config/configUtils.js.map +1 -1
  85. package/dist/dataLayer/backupManifest.d.ts +26 -0
  86. package/dist/dataLayer/backupManifest.js +97 -0
  87. package/dist/dataLayer/backupManifest.js.map +1 -0
  88. package/dist/dataLayer/blobBackup.d.ts +87 -0
  89. package/dist/dataLayer/blobBackup.js +282 -0
  90. package/dist/dataLayer/blobBackup.js.map +1 -0
  91. package/dist/dataLayer/harperBridge/ResourceBridge.d.ts +3 -0
  92. package/dist/dataLayer/harperBridge/ResourceBridge.js +45 -5
  93. package/dist/dataLayer/harperBridge/ResourceBridge.js.map +1 -1
  94. package/dist/dataLayer/hdbInfoController.js +4 -0
  95. package/dist/dataLayer/hdbInfoController.js.map +1 -1
  96. package/dist/dataLayer/restoreMarker.d.ts +122 -0
  97. package/dist/dataLayer/restoreMarker.js +261 -0
  98. package/dist/dataLayer/restoreMarker.js.map +1 -0
  99. package/dist/dataLayer/rocksdbBackup.d.ts +127 -0
  100. package/dist/dataLayer/rocksdbBackup.js +1039 -0
  101. package/dist/dataLayer/rocksdbBackup.js.map +1 -0
  102. package/dist/dataLayer/schemaDescribe.js +2 -1
  103. package/dist/dataLayer/schemaDescribe.js.map +1 -1
  104. package/dist/resources/DatabaseTransaction.d.ts +55 -0
  105. package/dist/resources/DatabaseTransaction.js +282 -109
  106. package/dist/resources/DatabaseTransaction.js.map +1 -1
  107. package/dist/resources/ResourceInterface.d.ts +8 -5
  108. package/dist/resources/ResourceInterface.js.map +1 -1
  109. package/dist/resources/Resources.js +22 -4
  110. package/dist/resources/Resources.js.map +1 -1
  111. package/dist/resources/Table.d.ts +6 -5
  112. package/dist/resources/Table.js +114 -37
  113. package/dist/resources/Table.js.map +1 -1
  114. package/dist/resources/analytics/write.js +6 -6
  115. package/dist/resources/analytics/write.js.map +1 -1
  116. package/dist/resources/blob.d.ts +8 -1
  117. package/dist/resources/blob.js +31 -14
  118. package/dist/resources/blob.js.map +1 -1
  119. package/dist/resources/databases.d.ts +59 -1
  120. package/dist/resources/databases.js +386 -41
  121. package/dist/resources/databases.js.map +1 -1
  122. package/dist/resources/jsResource.d.ts +4 -26
  123. package/dist/resources/jsResource.js +5 -59
  124. package/dist/resources/jsResource.js.map +1 -1
  125. package/dist/resources/models/Models.d.ts +11 -1
  126. package/dist/resources/models/Models.js +10 -1
  127. package/dist/resources/models/Models.js.map +1 -1
  128. package/dist/resources/models/backendRegistry.d.ts +9 -0
  129. package/dist/resources/models/backendRegistry.js +10 -0
  130. package/dist/resources/models/backendRegistry.js.map +1 -1
  131. package/dist/resources/models/openaiStream.d.ts +16 -1
  132. package/dist/resources/models/openaiStream.js +113 -21
  133. package/dist/resources/models/openaiStream.js.map +1 -1
  134. package/dist/resources/models/v1/chatCompletions.d.ts +54 -0
  135. package/dist/resources/models/v1/chatCompletions.js +115 -0
  136. package/dist/resources/models/v1/chatCompletions.js.map +1 -0
  137. package/dist/resources/models/v1/embeddings.d.ts +11 -0
  138. package/dist/resources/models/v1/embeddings.js +71 -0
  139. package/dist/resources/models/v1/embeddings.js.map +1 -0
  140. package/dist/resources/models/v1/errors.d.ts +54 -0
  141. package/dist/resources/models/v1/errors.js +130 -0
  142. package/dist/resources/models/v1/errors.js.map +1 -0
  143. package/dist/resources/models/v1/index.d.ts +36 -0
  144. package/dist/resources/models/v1/index.js +75 -0
  145. package/dist/resources/models/v1/index.js.map +1 -0
  146. package/dist/resources/models/v1/models.d.ts +26 -0
  147. package/dist/resources/models/v1/models.js +44 -0
  148. package/dist/resources/models/v1/models.js.map +1 -0
  149. package/dist/resources/models/v1/translation.d.ts +133 -0
  150. package/dist/resources/models/v1/translation.js +298 -0
  151. package/dist/resources/models/v1/translation.js.map +1 -0
  152. package/dist/resources/roles.d.ts +1 -1
  153. package/dist/resources/roles.js +54 -7
  154. package/dist/resources/roles.js.map +1 -1
  155. package/dist/resources/transaction.js +0 -3
  156. package/dist/resources/transaction.js.map +1 -1
  157. package/dist/security/jsLoader.js +84 -33
  158. package/dist/security/jsLoader.js.map +1 -1
  159. package/dist/security/role.js +4 -0
  160. package/dist/security/role.js.map +1 -1
  161. package/dist/security/superUserGuard.d.ts +7 -0
  162. package/dist/security/superUserGuard.js +23 -0
  163. package/dist/security/superUserGuard.js.map +1 -0
  164. package/dist/security/tokenAuthentication.d.ts +0 -1
  165. package/dist/security/tokenAuthentication.js +6 -2
  166. package/dist/security/tokenAuthentication.js.map +1 -1
  167. package/dist/security/user.d.ts +6 -1
  168. package/dist/security/user.js +23 -1
  169. package/dist/security/user.js.map +1 -1
  170. package/dist/server/REST.js +25 -9
  171. package/dist/server/REST.js.map +1 -1
  172. package/dist/server/fastifyRoutes.js +15 -1
  173. package/dist/server/fastifyRoutes.js.map +1 -1
  174. package/dist/server/http.d.ts +23 -2
  175. package/dist/server/http.js +99 -12
  176. package/dist/server/http.js.map +1 -1
  177. package/dist/server/itc/serverHandlers.js +7 -1
  178. package/dist/server/itc/serverHandlers.js.map +1 -1
  179. package/dist/server/jobs/jobProcess.js +20 -1
  180. package/dist/server/jobs/jobProcess.js.map +1 -1
  181. package/dist/server/jobs/jobRunner.js +10 -0
  182. package/dist/server/jobs/jobRunner.js.map +1 -1
  183. package/dist/server/jobs/jobs.js +11 -0
  184. package/dist/server/jobs/jobs.js.map +1 -1
  185. package/dist/server/middlewareChain.d.ts +10 -1
  186. package/dist/server/middlewareChain.js +81 -21
  187. package/dist/server/middlewareChain.js.map +1 -1
  188. package/dist/server/operationsServer.d.ts +0 -2
  189. package/dist/server/operationsServer.js.map +1 -1
  190. package/dist/server/serverHelpers/multipartParser.js +9 -0
  191. package/dist/server/serverHelpers/multipartParser.js.map +1 -1
  192. package/dist/server/serverHelpers/operationAuthorizationState.d.ts +2 -0
  193. package/dist/server/serverHelpers/operationAuthorizationState.js +13 -0
  194. package/dist/server/serverHelpers/operationAuthorizationState.js.map +1 -0
  195. package/dist/server/serverHelpers/registeredOperations.d.ts +3 -2
  196. package/dist/server/serverHelpers/registeredOperations.js +14 -13
  197. package/dist/server/serverHelpers/registeredOperations.js.map +1 -1
  198. package/dist/server/serverHelpers/serverHandlers.js +27 -7
  199. package/dist/server/serverHelpers/serverHandlers.js.map +1 -1
  200. package/dist/server/serverHelpers/serverUtilities.d.ts +1 -1
  201. package/dist/server/serverHelpers/serverUtilities.js +21 -7
  202. package/dist/server/serverHelpers/serverUtilities.js.map +1 -1
  203. package/dist/server/static.js +68 -21
  204. package/dist/server/static.js.map +1 -1
  205. package/dist/server/storageReclamation.d.ts +17 -0
  206. package/dist/server/storageReclamation.js +90 -6
  207. package/dist/server/storageReclamation.js.map +1 -1
  208. package/dist/server/threads/socketRouter.js +20 -0
  209. package/dist/server/threads/socketRouter.js.map +1 -1
  210. package/dist/server/threads/threadServer.js +12 -0
  211. package/dist/server/threads/threadServer.js.map +1 -1
  212. package/dist/sqlEngine/diff/differential.js +7 -3
  213. package/dist/sqlEngine/diff/differential.js.map +1 -1
  214. package/dist/sqlTranslator/index.js +6 -1
  215. package/dist/sqlTranslator/index.js.map +1 -1
  216. package/dist/upgrade/upgradePrompt.d.ts +2 -2
  217. package/dist/upgrade/upgradePrompt.js +22 -3
  218. package/dist/upgrade/upgradePrompt.js.map +1 -1
  219. package/dist/utility/OperationFunctionCaller.js +25 -6
  220. package/dist/utility/OperationFunctionCaller.js.map +1 -1
  221. package/dist/utility/errors/commonErrors.d.ts +1 -0
  222. package/dist/utility/errors/commonErrors.js +1 -0
  223. package/dist/utility/errors/commonErrors.js.map +1 -1
  224. package/dist/utility/hdbTerms.d.ts +13 -1
  225. package/dist/utility/hdbTerms.js +13 -1
  226. package/dist/utility/hdbTerms.js.map +1 -1
  227. package/dist/utility/install/installer.js +32 -0
  228. package/dist/utility/install/installer.js.map +1 -1
  229. package/dist/utility/logging/harper_logger.d.ts +47 -0
  230. package/dist/utility/logging/harper_logger.js +803 -38
  231. package/dist/utility/logging/harper_logger.js.map +1 -1
  232. package/dist/utility/operation_authorization.js +13 -3
  233. package/dist/utility/operation_authorization.js.map +1 -1
  234. package/npm-shrinkwrap.json +198 -193
  235. package/package.json +12 -7
  236. package/resources/DESIGN.md +2 -0
  237. package/resources/DatabaseTransaction.ts +310 -104
  238. package/resources/ResourceInterface.ts +8 -5
  239. package/resources/Resources.ts +22 -4
  240. package/resources/Table.ts +341 -263
  241. package/resources/analytics/write.ts +22 -20
  242. package/resources/blob.ts +31 -14
  243. package/resources/databases.ts +387 -46
  244. package/resources/jsResource.ts +5 -62
  245. package/resources/models/Models.ts +14 -1
  246. package/resources/models/backendRegistry.ts +10 -0
  247. package/resources/models/openaiStream.ts +131 -19
  248. package/resources/models/v1/chatCompletions.ts +128 -0
  249. package/resources/models/v1/embeddings.ts +70 -0
  250. package/resources/models/v1/errors.ts +141 -0
  251. package/resources/models/v1/index.ts +72 -0
  252. package/resources/models/v1/models.ts +53 -0
  253. package/resources/models/v1/translation.ts +362 -0
  254. package/resources/roles.ts +67 -7
  255. package/resources/transaction.ts +0 -3
  256. package/security/jsLoader.ts +84 -30
  257. package/security/role.ts +7 -0
  258. package/security/superUserGuard.ts +20 -0
  259. package/security/tokenAuthentication.ts +6 -3
  260. package/security/user.ts +26 -1
  261. package/server/DESIGN.md +61 -34
  262. package/server/REST.ts +25 -9
  263. package/server/fastifyRoutes.ts +20 -1
  264. package/server/http.ts +100 -13
  265. package/server/itc/serverHandlers.js +7 -1
  266. package/server/jobs/jobProcess.ts +18 -1
  267. package/server/jobs/jobRunner.ts +10 -0
  268. package/server/jobs/jobs.ts +11 -0
  269. package/server/middlewareChain.ts +79 -20
  270. package/server/operationsServer.ts +0 -2
  271. package/server/serverHelpers/multipartParser.ts +9 -0
  272. package/server/serverHelpers/operationAuthorizationState.ts +11 -0
  273. package/server/serverHelpers/registeredOperations.ts +19 -15
  274. package/server/serverHelpers/serverHandlers.js +28 -7
  275. package/server/serverHelpers/serverUtilities.ts +29 -7
  276. package/server/static.ts +87 -23
  277. package/server/storageReclamation.ts +104 -8
  278. package/server/threads/socketRouter.ts +20 -0
  279. package/server/threads/threadServer.js +11 -0
  280. package/sqlTranslator/index.ts +6 -1
  281. package/static/defaultConfig.yaml +3 -0
  282. package/studio/web/assets/{Chat-DK3GlWEb.js → Chat-aApwhRmz.js} +2 -2
  283. package/studio/web/assets/{Chat-DK3GlWEb.js.map → Chat-aApwhRmz.js.map} +1 -1
  284. package/studio/web/assets/{FloatingChat-fBcC1Ew_.js → FloatingChat-sC0H91n9.js} +4 -4
  285. package/studio/web/assets/{FloatingChat-fBcC1Ew_.js.map → FloatingChat-sC0H91n9.js.map} +1 -1
  286. package/studio/web/assets/{apiToken-DJo1nakA.js → apiToken-DZ8JhHOI.js} +2 -2
  287. package/studio/web/assets/{apiToken-DJo1nakA.js.map → apiToken-DZ8JhHOI.js.map} +1 -1
  288. package/studio/web/assets/{applications-BDfH8urd.js → applications-9SEVLIO5.js} +2 -2
  289. package/studio/web/assets/{applications-BDfH8urd.js.map → applications-9SEVLIO5.js.map} +1 -1
  290. package/studio/web/assets/{index-BHo3c2Gk.js → index-Dy3uDGXb.js} +6 -6
  291. package/studio/web/assets/index-Dy3uDGXb.js.map +1 -0
  292. package/studio/web/assets/{index.lazy-BTo0y6UM.js → index.lazy-CN1zq4I4.js} +4 -4
  293. package/studio/web/assets/{index.lazy-BTo0y6UM.js.map → index.lazy-CN1zq4I4.js.map} +1 -1
  294. package/studio/web/assets/{notifications-CMxvWNnz.js → notifications-BFAF07xr.js} +2 -2
  295. package/studio/web/assets/{notifications-CMxvWNnz.js.map → notifications-BFAF07xr.js.map} +1 -1
  296. package/studio/web/assets/{notifications-D3GoB26g.js → notifications-CKlYVvVN.js} +2 -2
  297. package/studio/web/assets/{notifications-D3GoB26g.js.map → notifications-CKlYVvVN.js.map} +1 -1
  298. package/studio/web/assets/{profile-Doj5FVDE.js → profile-A1zhEdFG.js} +2 -2
  299. package/studio/web/assets/{profile-Doj5FVDE.js.map → profile-A1zhEdFG.js.map} +1 -1
  300. package/studio/web/assets/{setComponentFile-yinsqJy0.js → setComponentFile-SEtBt_GV.js} +2 -2
  301. package/studio/web/assets/{setComponentFile-yinsqJy0.js.map → setComponentFile-SEtBt_GV.js.map} +1 -1
  302. package/studio/web/assets/{setup-DJwR0BHd.js → setup-BisINqdH.js} +2 -2
  303. package/studio/web/assets/{setup-DJwR0BHd.js.map → setup-BisINqdH.js.map} +1 -1
  304. package/studio/web/assets/{status-Br_AbsJs.js → status-BuQoCc7l.js} +2 -2
  305. package/studio/web/assets/{status-Br_AbsJs.js.map → status-BuQoCc7l.js.map} +1 -1
  306. package/studio/web/assets/{swagger-ui-react-02XH5sVf.js → swagger-ui-react-CAi_s1PC.js} +2 -2
  307. package/studio/web/assets/{swagger-ui-react-02XH5sVf.js.map → swagger-ui-react-CAi_s1PC.js.map} +1 -1
  308. package/studio/web/assets/{tsMode-D1DMKY7h.js → tsMode-DT74tlkM.js} +2 -2
  309. package/studio/web/assets/{tsMode-D1DMKY7h.js.map → tsMode-DT74tlkM.js.map} +1 -1
  310. package/studio/web/assets/{useEntityRestURL-DtRblRgw.js → useEntityRestURL-CGRGc1n7.js} +2 -2
  311. package/studio/web/assets/{useEntityRestURL-DtRblRgw.js.map → useEntityRestURL-CGRGc1n7.js.map} +1 -1
  312. package/studio/web/index.html +1 -1
  313. package/upgrade/upgradePrompt.ts +22 -3
  314. package/utility/OperationFunctionCaller.ts +24 -3
  315. package/utility/errors/commonErrors.ts +2 -0
  316. package/utility/hdbTerms.ts +13 -1
  317. package/utility/install/installer.ts +37 -0
  318. package/utility/logging/harper_logger.ts +786 -41
  319. package/utility/operation_authorization.ts +31 -3
  320. package/studio/web/assets/index-BHo3c2Gk.js.map +0 -1
package/bin/backup.ts ADDED
@@ -0,0 +1,169 @@
1
+ 'use strict';
2
+
3
+ import { createWriteStream } from 'node:fs';
4
+ import { rename, unlink } from 'node:fs/promises';
5
+ import { basename } from 'node:path';
6
+ import { pipeline } from 'node:stream/promises';
7
+ import * as YAML from 'yaml';
8
+ import * as envMgr from '../utility/environment/environmentManager.ts';
9
+ envMgr.initSync();
10
+ import * as terms from '../utility/hdbTerms.ts';
11
+ import { buildRequest, cliOperations, resolveRequestOptions } from './cliOperations.ts';
12
+ import { loadCredentials } from './cliCredentials.ts';
13
+ import { httpRequest } from '../utility/common_utils.ts';
14
+ import { initConfig } from '../config/configUtils.ts';
15
+ import { getHdbPid } from '../utility/processManagement/processManagement.js';
16
+ import {
17
+ createBackupOffline,
18
+ deleteBackupOffline,
19
+ listBackupsOffline,
20
+ purgeBackupsOffline,
21
+ restoreBackupOffline,
22
+ verifyBackupOffline,
23
+ } from '../dataLayer/rocksdbBackup.ts';
24
+
25
+ const { OPERATIONS_ENUM } = terms;
26
+
27
+ /**
28
+ * Runs a backup operation from the CLI. `command` is the operation name (e.g. `create_backup`);
29
+ * there is no hyphenated alias. Each works whether or not Harper is running: RocksDB is
30
+ * single-writer, so when a server is reachable the running process must own the database handle
31
+ * and the operation is forwarded to it (also handling remote `target=` and auth); only when the
32
+ * *local* server is stopped do we open the database/backup files directly. `get_backup` is the
33
+ * exception — it streams a live snapshot from a running server and has no offline form.
34
+ */
35
+ export async function runBackupCommand(command: string): Promise<void> {
36
+ // Load .env before anything reads process.env — useOperationApi and resolveRequestOptions key
37
+ // the local-vs-remote routing off HARPER_CLI_TARGET/CLI_TARGET, and without this a `.env`-
38
+ // configured remote target would be invisible, silently running a destructive op locally
39
+ // (cliOperations does the same on its first line).
40
+ require('dotenv').config();
41
+ const request = buildRequest();
42
+ const databaseName = request.database || 'data';
43
+
44
+ // We print results here (not by returning a string) so the CLI's top-level handler doesn't
45
+ // re-emit them through logger.notify — this keeps output identical to forwarding the operation
46
+ // to a running server, which the online branch below simply delegates to.
47
+ if (command === OPERATIONS_ENUM.GET_BACKUP) {
48
+ console.log(await downloadBackup(request, databaseName));
49
+ return;
50
+ }
51
+
52
+ // Fall back to direct file access only when there is no server to talk to (no target and the
53
+ // local instance is stopped). We never fall back on a *reachable* server's error response —
54
+ // e.g. restore_backup rejecting the system/component-held database (whose offline path is to
55
+ // stop the server and rerun this same command, which then takes the branch below).
56
+ if (useOperationApi(request)) {
57
+ request.operation = command;
58
+ await cliOperations(request); // prints its own result, exactly like any forwarded operation
59
+ return;
60
+ }
61
+
62
+ let result: any;
63
+ switch (command) {
64
+ case OPERATIONS_ENUM.CREATE_BACKUP:
65
+ result = await createBackupOffline(databaseName, request.exclude_blobs === true);
66
+ break;
67
+ case OPERATIONS_ENUM.LIST_BACKUPS:
68
+ result = await listBackupsOffline(databaseName);
69
+ break;
70
+ case OPERATIONS_ENUM.VERIFY_BACKUP:
71
+ result = await verifyBackupOffline(databaseName, request.backup_id, request.verify_checksum);
72
+ break;
73
+ case OPERATIONS_ENUM.DELETE_BACKUP:
74
+ result = await deleteBackupOffline(databaseName, request.backup_id);
75
+ break;
76
+ case OPERATIONS_ENUM.PURGE_BACKUPS:
77
+ result = await purgeBackupsOffline(databaseName, request.keep_count);
78
+ break;
79
+ case OPERATIONS_ENUM.RESTORE_BACKUP:
80
+ result = await restoreBackupOffline(databaseName, request.backup_id, request.target_database);
81
+ break;
82
+ default:
83
+ throw new Error(`Unknown backup command '${command}'`);
84
+ }
85
+ console.log(YAML.stringify(result).trim());
86
+ }
87
+
88
+ /**
89
+ * Whether the target of a saved `last_target` is the local instance (localhost / loopback). A
90
+ * `harper login` against a local server saves `http://localhost:9925/`, which must NOT be treated as
91
+ * a remote target — otherwise the offline backup commands would try to reach a stopped local server
92
+ * instead of falling back to direct file access.
93
+ */
94
+ function isLocalTarget(target: string): boolean {
95
+ try {
96
+ const url = new URL(target.includes('://') ? target : `http://${target}`);
97
+ const host = url.hostname.replace(/^\[|\]$/g, ''); // strip IPv6 brackets
98
+ return host === 'localhost' || host === '::1' || host.startsWith('127.');
99
+ } catch {
100
+ return false;
101
+ }
102
+ }
103
+
104
+ /**
105
+ * Whether to route a backup command through the operation API rather than direct file access. An
106
+ * explicit `target=`/env target, or a saved `last_target` pointing at a *remote* server, always
107
+ * implies talking to a server. A saved *local* `last_target` does not: the offline backup commands
108
+ * exist to work on local files while the local server is stopped, so a local/absent target falls
109
+ * through to the liveness check — the API only when the local instance is actually running
110
+ * (`getHdbPid` verifies process liveness, so a stale pid file reads as stopped).
111
+ */
112
+ function useOperationApi(request: any): boolean {
113
+ const lastTarget = loadCredentials()?.last_target;
114
+ if (
115
+ request.target ||
116
+ process.env.HARPER_CLI_TARGET ||
117
+ process.env.CLI_TARGET ||
118
+ (lastTarget && !isLocalTarget(lastTarget))
119
+ ) {
120
+ return true;
121
+ }
122
+ initConfig();
123
+ return Boolean(getHdbPid());
124
+ }
125
+
126
+ /**
127
+ * `get_backup` streams a full-snapshot tar of the database's current state from a running server
128
+ * into a local file. It works against a remote server (`target=…`) or the local instance, resolving
129
+ * the target and auth the same way as any other CLI operation; a local connection requires Harper to
130
+ * be running (there is no offline form). For RocksDB the stream is gzipped by default (pass
131
+ * `gzip=false` for a plain tar) and includes the database's file-backed blobs by default (pass
132
+ * `exclude_blobs=true` for an engine-only archive). While stopped, use `create_backup` — a local
133
+ * incremental directory backup — instead.
134
+ */
135
+ async function downloadBackup(request: any, databaseName: string): Promise<string> {
136
+ // only forward gzip/exclude_blobs when the user passed them — they are RocksDB-only options
137
+ // (sending them unconditionally would fail LMDB downloads), and when unset the server applies its
138
+ // own defaults (RocksDB gzips and includes blobs)
139
+ const body: any = { operation: terms.OPERATIONS_ENUM.GET_BACKUP, database: databaseName };
140
+ if (request.gzip !== undefined) body.gzip = request.gzip;
141
+ if (request.exclude_blobs !== undefined) body.exclude_blobs = request.exclude_blobs;
142
+ // resolveRequestOptions connects to a remote target (target=/env/saved) or the local domain
143
+ // socket with auth; for a local connection it enforces that Harper is running.
144
+ const { options } = await resolveRequestOptions(request);
145
+ options.streamResponse = true;
146
+ const response = await httpRequest(options, body);
147
+ if (response.statusCode !== 200) {
148
+ const chunks: Buffer[] = [];
149
+ for await (const chunk of response) chunks.push(Buffer.from(chunk));
150
+ throw new Error(`get_backup failed (${response.statusCode}): ${Buffer.concat(chunks).toString('utf8')}`);
151
+ }
152
+ // name the file after what the server is sending (RocksDB: <db>.tar[.gz]; LMDB: the database file).
153
+ // basename() strips any path from the server-supplied filename — a malicious/compromised target
154
+ // could otherwise return `../../…` and steer the write outside the cwd.
155
+ const serverFilename = response.headers['content-disposition']?.match(/filename="([^"]+)"/)?.[1];
156
+ const outputPath = request.out || (serverFilename && basename(serverFilename)) || `${databaseName}.backup`;
157
+ // Stream to a temporary sibling and rename on success, so a network/server failure mid-download
158
+ // neither truncates an existing known-good backup at `outputPath` nor leaves a partial file that
159
+ // looks complete. The temp name is unique per process+time so concurrent downloads never collide.
160
+ const tempPath = `${outputPath}.${process.pid}-${Date.now()}.part`;
161
+ try {
162
+ await pipeline(response, createWriteStream(tempPath, { flags: 'wx' }));
163
+ await rename(tempPath, outputPath);
164
+ } catch (error) {
165
+ await unlink(tempPath).catch(() => {});
166
+ throw error;
167
+ }
168
+ return `Backup of database '${databaseName}' written to ${outputPath}`;
169
+ }
@@ -389,9 +389,16 @@ async function refreshExpiredOperationToken(
389
389
  * @param skipResponseLog By default, the response is logged to the console. Set this to true to skip logging it, which can be useful for sensitive responses like login calls!
390
390
  * @returns {Promise<void>}
391
391
  */
392
- async function cliOperations(req: any, skipResponseLog = false) {
393
- require('dotenv').config();
394
-
392
+ /**
393
+ * Resolve the transport options for a CLI operation request: a remote target URL (with auth) when
394
+ * one is configured (`target=`, env, or a saved `last_target`), otherwise the local domain socket.
395
+ * Returns the `options` object ready for `httpRequest` (method + Content-Type, and an Authorization
396
+ * header for remote targets, refreshing an expired operation token when possible) plus the resolved
397
+ * `target` (undefined for a local connection). Exits the process if a local connection is required
398
+ * but Harper is not running or has no domain socket. Shared by `cliOperations` and the CLI's
399
+ * streaming `get_backup` download so both reach local and remote servers the same way.
400
+ */
401
+ export async function resolveRequestOptions(req: any): Promise<{ options: any; target: any }> {
395
402
  const allCredentials = loadCredentials();
396
403
  const rawTarget = resolveTarget(req, allCredentials);
397
404
  // Userinfo is a transport credential, and `normalizeTarget` strips it so the resolved target can
@@ -434,81 +441,93 @@ async function cliOperations(req: any, skipResponseLog = false) {
434
441
  process.exit(1);
435
442
  }
436
443
  }
437
- await PREPARE_OPERATION[req.operation]?.(req);
438
- try {
439
- let options = target ?? {
440
- protocol: 'http:',
441
- socketPath: getConfigPath(terms.CONFIG_PARAMS.OPERATIONSAPI_NETWORK_DOMAINSOCKET),
442
- };
443
- options.method = 'POST';
444
- options.headers = { 'Content-Type': 'application/json' };
445
- options.timeout = SSE_OPERATIONS.has(req.operation) ? SSE_OPERATION_TIMEOUT_MS : CLI_OPERATION_TIMEOUT_MS;
446
- // Authentication precedence: explicitly configured credentials (dedicated args, URL
447
- // userinfo, env vars) beat everything, then env-var tokens, then the saved `harper login`
448
- // token, and only then the legacy `username=`/`password=` payload fallback below. The
449
- // tokens must outrank that fallback: for add_user/alter_user those args are the credentials
450
- // of the user being created/altered, so treating them as auth would authenticate as a user
451
- // who doesn't exist yet (or as the wrong identity) instead of using the admin's session.
452
- const transportCredentials = target ? resolveTransportCredentials(req, urlCredentials) : undefined;
453
- if (transportCredentials) {
454
- options.headers.Authorization = basicAuthHeader(transportCredentials.username, transportCredentials.password);
455
- } else if (target) {
456
- // Bearer-token auth, for remote targets ONLY. A local operation goes over the domain
457
- // socket, which the server trusts via `bypassLocalAuth` but that bypass is an
458
- // `else if` on "no Authorization header present" (security/auth.ts), so attaching a
459
- // Bearer token to a local request opts out of the trust and gets validated instead,
460
- // 401ing on a token minted for some other cluster. Since these env vars are meant to
461
- // persist across a whole CI job (or a developer's shell), an ungated read here would
462
- // break every local `harper` command run in that environment.
463
- //
464
- // Env-var tokens (for CI/CD — see `harper login --for-ci`) take precedence over the
465
- // stored ~/.harperdb/credentials.json entry: they're an explicit per-invocation override
466
- // that needs no prior `harper login` on the runner. A token refreshed from env vars is
467
- // used in-memory only (there's no file to write back to); a token refreshed from the
468
- // credentials file is persisted as before.
469
- //
470
- // Whichever namespace supplies a token owns both halves. Resolving them independently
471
- // would let `HARPER_CLI_OPERATION_TOKEN` from one user pair with
472
- // `CLI_TARGET_REFRESH_TOKEN` from another: commands would run as the first identity
473
- // until its operation token expired, then silently continue as the second. `login.ts`
474
- // selects its username/password namespace as a unit for exactly this reason.
475
- const tokenPrefix = ['HARPER_CLI', 'CLI_TARGET'].find(
476
- (prefix) =>
477
- process.env[`${prefix}_OPERATION_TOKEN`] !== undefined || process.env[`${prefix}_REFRESH_TOKEN`] !== undefined
444
+ let options = target ?? {
445
+ protocol: 'http:',
446
+ socketPath: getConfigPath(terms.CONFIG_PARAMS.OPERATIONSAPI_NETWORK_DOMAINSOCKET),
447
+ };
448
+ options.method = 'POST';
449
+ options.headers = { 'Content-Type': 'application/json' };
450
+ options.timeout = SSE_OPERATIONS.has(req.operation) ? SSE_OPERATION_TIMEOUT_MS : CLI_OPERATION_TIMEOUT_MS;
451
+ // Authentication precedence: explicitly configured credentials (dedicated args, URL
452
+ // userinfo, env vars) beat everything, then env-var tokens, then the saved `harper login`
453
+ // token, and only then the legacy `username=`/`password=` payload fallback below. The
454
+ // tokens must outrank that fallback: for add_user/alter_user those args are the credentials
455
+ // of the user being created/altered, so treating them as auth would authenticate as a user
456
+ // who doesn't exist yet (or as the wrong identity) instead of using the admin's session.
457
+ const transportCredentials = target ? resolveTransportCredentials(req, urlCredentials) : undefined;
458
+ if (transportCredentials) {
459
+ options.headers.Authorization = basicAuthHeader(transportCredentials.username, transportCredentials.password);
460
+ } else if (target) {
461
+ // Bearer-token auth, for remote targets ONLY. A local operation goes over the domain
462
+ // socket, which the server trusts via `bypassLocalAuth` — but that bypass is an
463
+ // `else if` on "no Authorization header present" (security/auth.ts), so attaching a
464
+ // Bearer token to a local request opts out of the trust and gets validated instead,
465
+ // 401ing on a token minted for some other cluster. Since these env vars are meant to
466
+ // persist across a whole CI job (or a developer's shell), an ungated read here would
467
+ // break every local `harper` command run in that environment.
468
+ //
469
+ // Env-var tokens (for CI/CD — see `harper login --for-ci`) take precedence over the
470
+ // stored ~/.harperdb/credentials.json entry: they're an explicit per-invocation override
471
+ // that needs no prior `harper login` on the runner. A token refreshed from env vars is
472
+ // used in-memory only (there's no file to write back to); a token refreshed from the
473
+ // credentials file is persisted as before.
474
+ //
475
+ // Whichever namespace supplies a token owns both halves. Resolving them independently
476
+ // would let `HARPER_CLI_OPERATION_TOKEN` from one user pair with
477
+ // `CLI_TARGET_REFRESH_TOKEN` from another: commands would run as the first identity
478
+ // until its operation token expired, then silently continue as the second. `login.ts`
479
+ // selects its username/password namespace as a unit for exactly this reason.
480
+ const tokenPrefix = ['HARPER_CLI', 'CLI_TARGET'].find(
481
+ (prefix) =>
482
+ process.env[`${prefix}_OPERATION_TOKEN`] !== undefined || process.env[`${prefix}_REFRESH_TOKEN`] !== undefined
483
+ );
484
+ const envOperationToken = tokenPrefix ? process.env[`${tokenPrefix}_OPERATION_TOKEN`]?.trim() : undefined;
485
+ const envRefreshToken = tokenPrefix ? process.env[`${tokenPrefix}_REFRESH_TOKEN`]?.trim() : undefined;
486
+ // A namespace that is set but blank is a broken CI secret, not a request to fall back to
487
+ // whatever the developer last logged in as — say so rather than switching identity silently.
488
+ if (tokenPrefix && !envOperationToken && !envRefreshToken) {
489
+ console.error(
490
+ `Ignoring empty ${tokenPrefix}_OPERATION_TOKEN/${tokenPrefix}_REFRESH_TOKEN; falling back to saved login credentials.`
478
491
  );
479
- const envOperationToken = tokenPrefix ? process.env[`${tokenPrefix}_OPERATION_TOKEN`]?.trim() : undefined;
480
- const envRefreshToken = tokenPrefix ? process.env[`${tokenPrefix}_REFRESH_TOKEN`]?.trim() : undefined;
481
- // A namespace that is set but blank is a broken CI secret, not a request to fall back to
482
- // whatever the developer last logged in as — say so rather than switching identity silently.
483
- if (tokenPrefix && !envOperationToken && !envRefreshToken) {
484
- console.error(
485
- `Ignoring empty ${tokenPrefix}_OPERATION_TOKEN/${tokenPrefix}_REFRESH_TOKEN; falling back to saved login credentials.`
486
- );
487
- }
492
+ }
488
493
 
489
- let tokens: { operation_token?: string; refresh_token?: string } | null = null;
490
- let persistKey: string | null = null; // non-null => persist a refreshed operation token back to the file
491
- if (envOperationToken || envRefreshToken) {
492
- tokens = { operation_token: envOperationToken, refresh_token: envRefreshToken };
493
- } else if (allCredentials?.targets) {
494
- persistKey = target.resolvedTarget;
495
- tokens = allCredentials.targets[persistKey] ?? null;
496
- }
494
+ let tokens: { operation_token?: string; refresh_token?: string } | null = null;
495
+ let persistKey: string | null = null; // non-null => persist a refreshed operation token back to the file
496
+ if (envOperationToken || envRefreshToken) {
497
+ tokens = { operation_token: envOperationToken, refresh_token: envRefreshToken };
498
+ } else if (allCredentials?.targets) {
499
+ persistKey = target.resolvedTarget;
500
+ tokens = allCredentials.targets[persistKey] ?? null;
501
+ }
497
502
 
498
- if (tokens?.operation_token || tokens?.refresh_token) {
499
- await refreshExpiredOperationToken(options, tokens, persistKey);
500
- if (tokens.operation_token) {
501
- options.headers.Authorization = `Bearer ${tokens.operation_token}`;
502
- }
503
+ if (tokens?.operation_token || tokens?.refresh_token) {
504
+ await refreshExpiredOperationToken(options, tokens, persistKey);
505
+ if (tokens.operation_token) {
506
+ options.headers.Authorization = `Bearer ${tokens.operation_token}`;
503
507
  }
504
508
  }
505
- // Legacy fallback for operations where `username=`/`password=` genuinely ARE the caller's
506
- // credentials (e.g. `create_table username= password=`) and nothing else is configured.
507
- // Both are required a lone `username=` (as in `drop_user username=bob`) is payload, not
508
- // a credential.
509
- if (target && !options.headers.Authorization && req.username && req.password) {
510
- options.headers.Authorization = basicAuthHeader(req.username, req.password);
511
- }
509
+ }
510
+ // Legacy fallback for operations where `username=`/`password=` genuinely ARE the caller's
511
+ // credentials (e.g. `create_table username= password=`) and nothing else is configured.
512
+ // Both are required — a lone `username=` (as in `drop_user username=bob`) is payload, not
513
+ // a credential.
514
+ if (target && !options.headers.Authorization && req.username && req.password) {
515
+ options.headers.Authorization = basicAuthHeader(req.username, req.password);
516
+ }
517
+ return { options, target };
518
+ }
519
+
520
+ async function cliOperations(req: any, skipResponseLog = false) {
521
+ require('dotenv').config();
522
+
523
+ // Resolve target/auth inside the try so a credential or connection error (e.g. an incomplete
524
+ // `auth_username=`/`auth_password=` pair, which resolveRequestOptions throws on) is mapped to the
525
+ // same console.error + process.exit(1) as every other failure below, rather than escaping as an
526
+ // unhandled rejection. `target` is declared out here so the catch can still reference it.
527
+ let options: any, target: any;
528
+ try {
529
+ ({ options, target } = await resolveRequestOptions(req));
530
+ await PREPARE_OPERATION[req.operation]?.(req);
512
531
  // Streaming deploy (multipart upload + SSE progress) only works against >= 5.1 servers.
513
532
  // When deploying to a remote target, probe its version first and downgrade to the
514
533
  // legacy JSON deploy if it predates 5.1. Local (domain-socket) deploys always
package/bin/copyDb.ts CHANGED
@@ -1,4 +1,10 @@
1
- import { getDatabases, getDefaultCompression, resetDatabases } from '../resources/databases.ts';
1
+ import {
2
+ getDatabases,
3
+ getDefaultCompression,
4
+ resetDatabases,
5
+ getRocksCompression,
6
+ toRocksCompression,
7
+ } from '../resources/databases.ts';
2
8
  import { open, asBinary } from 'lmdb';
3
9
  import { join } from 'path';
4
10
  import { move, remove } from 'fs-extra';
@@ -329,6 +335,10 @@ export function shapeForStructure(value: any): any {
329
335
 
330
336
  function openRocksDb(path: string, options: RocksDatabaseOptions & { dupSort?: boolean } = {}) {
331
337
  options.disableWAL ??= false;
338
+ // Migration creates a complete replacement database, so use the deployment codec for the files
339
+ // it writes; runtime opens additionally reconcile pre-existing sibling column families.
340
+ const legacyOptions = options as { compression?: unknown };
341
+ legacyOptions.compression = getRocksCompression() ?? toRocksCompression(legacyOptions.compression);
332
342
  if (!existsSync(path)) {
333
343
  mkdirSync(path, { recursive: true });
334
344
  }
@@ -510,7 +520,14 @@ export async function migrateDatabaseToRocks(sourceRootStore, databaseName: stri
510
520
  export async function copyDbToRocks(sourceRootStore, sourceDatabase: string, targetPath: string) {
511
521
  console.log(`Migrating database ${sourceDatabase} to RocksDB at ${targetPath}`);
512
522
  const sourceDbisDb = sourceRootStore.dbisDb;
513
-
523
+ // Runtime Harper stores disable RocksDB's native WAL for data/index column families and recover
524
+ // them from rocksdb-js transaction logs. This copier does not write those transaction logs, so
525
+ // only use the same fast write path for migrateOnStart's disposable staging directory: after an
526
+ // interruption it is deleted and recopied from LMDB. Direct copyDbToRocks callers retain WAL.
527
+ const disableDataWAL = targetPath.endsWith(MIGRATING_DIR_SUFFIX);
528
+
529
+ // Keep native WAL for the root/log-owner and __dbis__ handles, matching Harper's runtime policy.
530
+ // Their migration writes are metadata-sized; the 600 GB bulk is in the data/index handles below.
514
531
  const targetRootStore = openRocksDb(targetPath, { disableWAL: false });
515
532
  // Every handle opened on targetPath. All must be closed before returning so the caller can
516
533
  // atomically rename a staging directory into place — rocksdb-js registers descriptors by
@@ -587,10 +604,10 @@ export async function copyDbToRocks(sourceRootStore, sourceDatabase: string, tar
587
604
  let observerEncoder: any;
588
605
  let canonicalStructures: any;
589
606
  if (!isPrimary) {
590
- targetDbi = openRocksDb(targetPath, { dupSort: true, name: key });
607
+ targetDbi = openRocksDb(targetPath, { disableWAL: disableDataWAL, dupSort: true, name: key });
591
608
  targetHandles.push(targetDbi);
592
609
  } else {
593
- targetDbi = openRocksDb(targetPath, { name: key });
610
+ targetDbi = openRocksDb(targetPath, { disableWAL: disableDataWAL, name: key });
594
611
  targetHandles.push(targetDbi);
595
612
  // Patch the existing encoder (encoder is a getter-only property on RocksDatabase, cannot be replaced)
596
613
  // to install RecordEncoder's encode method so metadata headers (timestamps, HAS_BLOBS flag) are written
package/bin/harper.ts CHANGED
@@ -5,62 +5,39 @@ import * as fs from 'node:fs';
5
5
  import * as path from 'node:path';
6
6
  import logger from '../utility/logging/harper_logger.ts';
7
7
  import * as cliOperations from './cliOperations.ts';
8
+ import { help } from './help.ts';
8
9
  import { packageJson } from '../utility/packageUtils.js';
9
10
  import checkNode from '../launchServiceScripts/utility/checkNodeVersion.js';
10
11
  import * as hdbTerms from '../utility/hdbTerms.ts';
11
- const { SERVICE_ACTIONS_ENUM } = hdbTerms as any;
12
+ const { SERVICE_ACTIONS_ENUM, OPERATIONS_ENUM } = hdbTerms as any;
12
13
  if (typeof process.setSourceMapsEnabled === 'function') {
13
14
  process.setSourceMapsEnabled(true); // this is necessary for source maps to work, at least on the main thread.
14
15
  }
15
16
 
16
- const HELP = `
17
- Usage: harperdb [command]
18
-
19
- With no command, harper will simply run Harper (in the foreground)
20
-
21
- Documentation: https://docs.harperdb.io/
22
-
23
- By default, the CLI also supports certain Operation APIs. Specify the operation name and any required parameters, and omit the 'operation' command.
17
+ /**
18
+ * Format a CLI error for the terminal. Expected, user-facing errors (a `ClientError` from an
19
+ * operation — bad args, not found, a locked backup repo — which carry a numeric `statusCode`) get
20
+ * just their message, not a Node stack trace. A genuinely unexpected error keeps its stack so a bug
21
+ * is still debuggable. Mirrors the clean `error: <message>` output of a forwarded operation.
22
+ */
23
+ export function formatCliError(error: any): string {
24
+ const message = `error: ${error?.message ?? error}`;
25
+ if (error?.stack && typeof error?.statusCode !== 'number') return `${message}\n${error.stack}`;
26
+ return message;
27
+ }
24
28
 
25
- Commands:
26
- agent [message] - Chat with the built-in agent (interactive, or one-shot with a message; alias: chat)
27
- copy-db <source> <target> - Copies a database from source path to target path
28
- dev <path> - Run the application in dev mode with debugging, foreground logging, no auth
29
- install - Install harperdb
30
- <api-operation> <param>=<value> - Run an API operation and return result to the CLI, not all operations are supported
31
- To authenticate as a different user than the one being operated on
32
- (e.g. add_user/alter_user), set HARPER_CLI_USERNAME/HARPER_CLI_PASSWORD
33
- or run 'harper login'. The equivalent auth_username=<value>
34
- auth_password=<value> args also work, but a password passed as an
35
- argument is exposed in shell history, process listings and CI logs.
36
- A saved login token always outranks username=/password=, so a
37
- stale token that fails to refresh will 401 rather than falling
38
- back to them — run 'harper logout' or pass auth_username=/
39
- auth_password= to override it.
40
- login [target] [username] - Login to a remote or local Harper instance
41
- --for-ci prints the CI/CD credentials (target + long-lived
42
- refresh token) to stdout in dotenv format, and everything else
43
- to stderr, so it pipes without the token hitting your screen:
44
- harper login --for-ci | gh secret set --env-file -
45
- Log in as a user dedicated to that one CI consumer: Harper
46
- stores a single refresh token per user, so this revokes any
47
- refresh token that user already holds — another runner, another
48
- machine, or an earlier 'harper login' will 401 on its next
49
- refresh. Two consumers cannot share a user.
50
- logout [target] - Logout from Harper and clear saved JWT
51
- mcp [subcommand] - MCP stdio bridge / print-config / doctor (see 'harper mcp help')
52
- register - Register harperdb
53
- renew-certs - Generate a new set of self-signed certificates
54
- restart - Restart the harperdb background process
55
- run <path> - Run the application in the specified path
56
- start - Starts a separate background process for harperdb and CLI will exit
57
- status - Print the status of Harper
58
- stop - Stop the harperdb background process
59
- help - Display this output
60
- upgrade - Upgrade harperdb
61
- version - Print the version
62
- deploy - Deploy the application locally or remotely with target=<remote url>
63
- `;
29
+ /**
30
+ * Whether a `-h`/`--help` anywhere in `argv` should print the top-level help. Returns false for the
31
+ * subcommands that own their own `--help` mcp and agent/chat parse `process.argv.slice(3)`
32
+ * themselves downstream so `harper mcp --help` reaches the mcp handler instead of this help.
33
+ */
34
+ export function wantsTopLevelHelp(argv: readonly string[], service: string | undefined): boolean {
35
+ const delegatesHelp =
36
+ service === SERVICE_ACTIONS_ENUM.MCP ||
37
+ service === SERVICE_ACTIONS_ENUM.AGENT ||
38
+ service === SERVICE_ACTIONS_ENUM.CHAT;
39
+ return !delegatesHelp && (argv.includes('-h') || argv.includes('--help'));
40
+ }
64
41
 
65
42
  async function harper() {
66
43
  let nodeResults = checkNode();
@@ -78,13 +55,17 @@ async function harper() {
78
55
 
79
56
  let service;
80
57
 
81
- if (process.argv && process.argv[2] && !process.argv[2].startsWith('-')) {
58
+ if (process.argv?.[2] && !process.argv[2].startsWith('-')) {
82
59
  service = process.argv[2].toLowerCase();
83
60
  }
84
61
 
62
+ if (wantsTopLevelHelp(process.argv, service)) {
63
+ return help();
64
+ }
65
+
85
66
  switch (service) {
86
67
  case SERVICE_ACTIONS_ENUM.HELP:
87
- return HELP;
68
+ return help();
88
69
  case SERVICE_ACTIONS_ENUM.START:
89
70
  return require('./run').launch();
90
71
  case SERVICE_ACTIONS_ENUM.INSTALL:
@@ -140,6 +121,14 @@ async function harper() {
140
121
  let targetDbPath = process.argv[4];
141
122
  return require('./copyDb').copyDb(sourceDb, targetDbPath);
142
123
  }
124
+ case OPERATIONS_ENUM.CREATE_BACKUP:
125
+ case OPERATIONS_ENUM.LIST_BACKUPS:
126
+ case OPERATIONS_ENUM.VERIFY_BACKUP:
127
+ case OPERATIONS_ENUM.DELETE_BACKUP:
128
+ case OPERATIONS_ENUM.PURGE_BACKUPS:
129
+ case OPERATIONS_ENUM.GET_BACKUP:
130
+ case OPERATIONS_ENUM.RESTORE_BACKUP:
131
+ return require('./backup').runBackupCommand(service);
143
132
  case SERVICE_ACTIONS_ENUM.DEV:
144
133
  process.env.DEV_MODE = 'true';
145
134
  // fall through
@@ -191,18 +180,17 @@ if (require.main === module) {
191
180
  harper()
192
181
  .then((message) => {
193
182
  if (message) {
183
+ // console.log is the canonical terminal output for CLI results; logger.notify would
184
+ // print the same message a second time (its Console transport is stdout in CLI mode),
185
+ // so `harper help` and friends were emitted twice.
194
186
  console.log(message);
195
- logger.notify(message);
196
187
  }
197
188
  // Intentionally not calling `process.exit(0);` so if a CLI
198
189
  // command resulted in a long running process (aka `run`),
199
190
  // it continues to run.
200
191
  })
201
192
  .catch((error) => {
202
- if (error) {
203
- console.error(error);
204
- logger.error(error);
205
- }
193
+ if (error) console.error(formatCliError(error));
206
194
  process.exit(1);
207
195
  });
208
196
  }