@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/help.ts ADDED
@@ -0,0 +1,216 @@
1
+ /**
2
+ * The `harper help` / `harper -h` output, kept as structured data so {@link help} can render it to
3
+ * the current terminal width (capped at 120 columns) at call time. Content lives separately from
4
+ * layout: editing the text can't break the wrapping, command names stay column-aligned across
5
+ * sections, and the CI/CD sample stays a verbatim, copy-pasteable line at any width.
6
+ */
7
+
8
+ const MAX_WIDTH = 120;
9
+ const INDENT = 2; // leading spaces for command names and detail blocks
10
+ const DASH = ' - '; // separator between a command name and its description
11
+ // Minimum description width for the two-column command layout. On terminals too narrow for it
12
+ // (width < descCol + this), command rows stack the name above an indented description instead of
13
+ // overflowing. Set to the widest word in any command description so two-column never overflows.
14
+ const MIN_DESC_WIDTH = 20;
15
+
16
+ /** A paragraph of prose, reflowed to the target width at `indent` spaces. */
17
+ interface TextBlock {
18
+ text: string;
19
+ indent?: number;
20
+ }
21
+
22
+ /** A verbatim line (a code sample); never joined or wrapped so it stays copy-pasteable. */
23
+ interface PreBlock {
24
+ pre: string;
25
+ indent?: number;
26
+ }
27
+
28
+ /** A two-column list of `[name, description]` rows; descriptions wrap with a hanging indent. */
29
+ interface CommandsBlock {
30
+ commands: [name: string, description: string][];
31
+ }
32
+
33
+ type Block = TextBlock | PreBlock | CommandsBlock;
34
+
35
+ interface Section {
36
+ heading?: string;
37
+ blocks: Block[];
38
+ }
39
+
40
+ const SECTIONS: Section[] = [
41
+ { blocks: [{ text: 'Usage: harperdb [command]' }] },
42
+ { blocks: [{ text: 'With no command, harper will simply run Harper (in the foreground)' }] },
43
+ { blocks: [{ text: 'Documentation: https://docs.harperdb.io/' }] },
44
+ {
45
+ blocks: [
46
+ {
47
+ text: [
48
+ 'By default, the CLI also supports certain Operation APIs. Specify the operation name and any required',
49
+ "parameters, and omit the 'operation' command.",
50
+ ].join(' '),
51
+ },
52
+ ],
53
+ },
54
+ { blocks: [{ text: 'Commands:' }] },
55
+ {
56
+ heading: 'Server',
57
+ blocks: [
58
+ {
59
+ commands: [
60
+ ['start', 'Starts a separate background process for harperdb and CLI will exit'],
61
+ ['stop', 'Stop the harperdb background process'],
62
+ ['restart', 'Restart the harperdb background process'],
63
+ ['status', 'Print the status of Harper'],
64
+ ],
65
+ },
66
+ ],
67
+ },
68
+ {
69
+ heading: 'Applications',
70
+ blocks: [
71
+ {
72
+ commands: [
73
+ ['run <path>', 'Run the application in the specified path'],
74
+ ['dev <path>', 'Run the application in dev mode with debugging, foreground logging, no auth'],
75
+ ['deploy', 'Deploy the application locally or remotely with target=<remote url>'],
76
+ ],
77
+ },
78
+ ],
79
+ },
80
+ {
81
+ heading: 'Install & maintenance',
82
+ blocks: [
83
+ {
84
+ commands: [
85
+ ['install', 'Install harperdb'],
86
+ ['upgrade', 'Upgrade harperdb'],
87
+ ['register', 'Register harperdb'],
88
+ ['renew-certs', 'Generate a new set of self-signed certificates'],
89
+ ['copy-db <source> <target>', 'Copies a database from source path to target path'],
90
+ ['version', 'Print the version'],
91
+ ['help', 'Display this output'],
92
+ ],
93
+ },
94
+ ],
95
+ },
96
+ {
97
+ heading: 'Accounts',
98
+ blocks: [
99
+ { pre: 'login [target] [username]', indent: INDENT },
100
+ {
101
+ text: [
102
+ 'Login to a remote or local Harper instance. --for-ci prints the CI/CD credentials (target +',
103
+ 'long-lived refresh token) to stdout in dotenv format, and everything else to stderr, so it pipes',
104
+ 'without the token hitting your screen:',
105
+ ].join(' '),
106
+ indent: 4,
107
+ },
108
+ { pre: 'harper login --for-ci | gh secret set --env-file -', indent: 6 },
109
+ {
110
+ text: [
111
+ 'Log in as a user dedicated to that one CI consumer: Harper stores a single refresh token per user, so',
112
+ 'this revokes any refresh token that user already holds — another runner, another machine, or an earlier',
113
+ "'harper login' will 401 on its next refresh. Two consumers cannot share a user.",
114
+ ].join(' '),
115
+ indent: 4,
116
+ },
117
+ { commands: [['logout [target]', 'Logout from Harper and clear saved JWT']] },
118
+ ],
119
+ },
120
+ {
121
+ heading: 'Assistants',
122
+ blocks: [
123
+ {
124
+ commands: [
125
+ ['agent [message]', 'Chat with the built-in agent (interactive, or one-shot with a message; alias: chat)'],
126
+ ['mcp [subcommand]', "MCP stdio bridge / print-config / doctor (see 'harper mcp help')"],
127
+ ],
128
+ },
129
+ ],
130
+ },
131
+ {
132
+ heading: 'Operations API',
133
+ blocks: [
134
+ { pre: '<api-operation> <param>=<value>', indent: INDENT },
135
+ {
136
+ text: [
137
+ 'Run an API operation and return the result to the CLI (not all operations are supported). See the full',
138
+ 'list of operations at: https://docs.harperdb.io/reference/v5/operations-api/operations',
139
+ ].join(' '),
140
+ indent: 4,
141
+ },
142
+ {
143
+ text: [
144
+ 'To authenticate as a different user than the one being operated on (e.g. add_user/alter_user), set',
145
+ "HARPER_CLI_USERNAME/HARPER_CLI_PASSWORD or run 'harper login'. The equivalent auth_username=<value>",
146
+ 'auth_password=<value> args also work, but a password passed as an argument is exposed in shell',
147
+ 'history, process listings and CI logs. A saved login token always outranks username=/password=, so a',
148
+ "stale token that fails to refresh will 401 rather than falling back to them — run 'harper logout' or",
149
+ 'pass auth_username=/auth_password= to override it.',
150
+ ].join(' '),
151
+ indent: 4,
152
+ },
153
+ ],
154
+ },
155
+ ];
156
+
157
+ const pad = (count: number): string => ' '.repeat(count);
158
+
159
+ /** Greedy word wrap. Never splits a token, so URLs and flags survive intact (they overflow). */
160
+ function wrap(text: string, width: number): string[] {
161
+ const words = text.split(/\s+/).filter(Boolean);
162
+ if (words.length === 0) return [];
163
+ const lines: string[] = [];
164
+ let line = words[0];
165
+ for (let i = 1; i < words.length; i++) {
166
+ if (line.length + 1 + words[i].length > width) {
167
+ lines.push(line);
168
+ line = words[i];
169
+ } else {
170
+ line += ' ' + words[i];
171
+ }
172
+ }
173
+ lines.push(line);
174
+ return lines;
175
+ }
176
+
177
+ function renderBlock(block: Block, width: number, nameWidth: number, descCol: number): string[] {
178
+ if ('commands' in block) {
179
+ // Two columns when the terminal leaves a readable description column; otherwise stack the name
180
+ // above an indented description so narrow terminals don't overflow the command rows.
181
+ const twoColumn = width - descCol >= MIN_DESC_WIDTH;
182
+ return block.commands.flatMap(([name, description]) => {
183
+ if (twoColumn) {
184
+ const wrapped = wrap(description, width - descCol);
185
+ const head = pad(INDENT) + name.padEnd(nameWidth) + DASH;
186
+ return [head + (wrapped[0] ?? ''), ...wrapped.slice(1).map((line) => pad(descCol) + line)];
187
+ }
188
+ const detail = INDENT * 2;
189
+ return [pad(INDENT) + name, ...wrap(description, Math.max(1, width - detail)).map((line) => pad(detail) + line)];
190
+ });
191
+ }
192
+ if ('pre' in block) return [pad(block.indent ?? 0) + block.pre];
193
+ const indent = block.indent ?? 0;
194
+ return wrap(block.text, Math.max(1, width - indent)).map((line) => pad(indent) + line);
195
+ }
196
+
197
+ /** Render the CLI help, wrapped to the terminal width (capped at 120, and 120 when not a TTY). */
198
+ export function help(): string {
199
+ const width = Math.min(process.stdout.columns || MAX_WIDTH, MAX_WIDTH);
200
+
201
+ // One shared description column across every command list so names line up between sections,
202
+ // driven by the longest command name.
203
+ const names = SECTIONS.flatMap((section) =>
204
+ section.blocks.flatMap((block) => ('commands' in block ? block.commands.map(([name]) => name) : []))
205
+ );
206
+ const nameWidth = Math.max(...names.map((name) => name.length));
207
+ const descCol = INDENT + nameWidth + DASH.length;
208
+
209
+ const lines: string[] = [];
210
+ for (const section of SECTIONS) {
211
+ if (lines.length) lines.push(''); // blank line between sections
212
+ if (section.heading) lines.push(section.heading);
213
+ for (const block of section.blocks) lines.push(...renderBlock(block, width, nameWidth, descCol));
214
+ }
215
+ return '\n' + lines.join('\n') + '\n';
216
+ }
@@ -483,10 +483,106 @@ const COMPONENT_PREPARATION_WAIT_MARGIN_MS = 30000;
483
483
  const MAX_GIT_EXTRACTION_COMMANDS = 4;
484
484
  const MAX_INSTALL_COMMANDS = 2;
485
485
 
486
+ type ExtractionTransaction = {
487
+ commit(): Promise<void>;
488
+ rollback(): Promise<void>;
489
+ };
490
+
486
491
  // The credential helper git executes for a private git-reference deploy. It ships alongside this
487
492
  // module (both in source and in dist), holds no secret, and is inert without a live session.
488
493
  export const GIT_CREDENTIAL_HELPER_PATH = join(__dirname, 'gitCredentialHelper.js');
489
494
 
495
+ const PACKAGE_LOCK_FILES = [
496
+ 'package-lock.json',
497
+ 'npm-shrinkwrap.json',
498
+ 'pnpm-lock.yaml',
499
+ 'yarn.lock',
500
+ 'bun.lock',
501
+ 'bun.lockb',
502
+ ];
503
+
504
+ type InstalledPackageMetadata = {
505
+ files: Map<string, Buffer>;
506
+ readable: boolean;
507
+ hasLockfile: boolean;
508
+ hasInstallableDependencies: boolean;
509
+ };
510
+
511
+ export async function readInstalledPackageMetadata(directory: string): Promise<InstalledPackageMetadata> {
512
+ const files = new Map<string, Buffer>();
513
+ let readable = true;
514
+ let packageJSON: any;
515
+ await Promise.all([
516
+ (async () => {
517
+ try {
518
+ const contents = await readFile(join(directory, 'package.json'));
519
+ try {
520
+ packageJSON = JSON.parse(contents.toString());
521
+ files.set('package.json', Buffer.from(JSON.stringify(canonicalizeJSON(packageJSON))));
522
+ } catch {
523
+ files.set('package.json', contents);
524
+ }
525
+ } catch (error) {
526
+ if ((error as NodeJS.ErrnoException).code !== 'ENOENT') readable = false;
527
+ }
528
+ })(),
529
+ ...PACKAGE_LOCK_FILES.map(async (filename) => {
530
+ try {
531
+ files.set(filename, await readFile(join(directory, filename)));
532
+ } catch (error) {
533
+ if ((error as NodeJS.ErrnoException).code !== 'ENOENT') readable = false;
534
+ }
535
+ }),
536
+ ]);
537
+ return {
538
+ files,
539
+ readable,
540
+ hasLockfile: PACKAGE_LOCK_FILES.some((filename) => files.has(filename)),
541
+ hasInstallableDependencies: ['dependencies', 'devDependencies', 'optionalDependencies', 'peerDependencies'].some(
542
+ (field) => {
543
+ const dependencies = packageJSON?.[field];
544
+ return (
545
+ typeof dependencies === 'object' &&
546
+ dependencies !== null &&
547
+ !Array.isArray(dependencies) &&
548
+ Object.keys(dependencies).length > 0
549
+ );
550
+ }
551
+ ),
552
+ };
553
+ }
554
+
555
+ export function installedPackageMetadataEqual(
556
+ previous: InstalledPackageMetadata,
557
+ current: InstalledPackageMetadata
558
+ ): boolean {
559
+ if (!previous.readable || !current.readable || previous.files.size !== current.files.size) return false;
560
+ for (const [filename, contents] of previous.files) {
561
+ if (!current.files.get(filename)?.equals(contents)) return false;
562
+ }
563
+ return true;
564
+ }
565
+
566
+ export function installedRuntimeChanged(
567
+ previous: InstalledPackageMetadata,
568
+ current: InstalledPackageMetadata,
569
+ installationIsOpaque: boolean
570
+ ): boolean {
571
+ return (
572
+ installationIsOpaque ||
573
+ (current.hasInstallableDependencies && !current.hasLockfile) ||
574
+ !installedPackageMetadataEqual(previous, current)
575
+ );
576
+ }
577
+
578
+ function canonicalizeJSON(value: any): any {
579
+ if (Array.isArray(value)) return value.map(canonicalizeJSON);
580
+ if (!value || typeof value !== 'object') return value;
581
+ const canonical: Record<string, any> = Object.create(null);
582
+ for (const key of Object.keys(value).sort()) canonical[key] = canonicalizeJSON(value[key]);
583
+ return canonical;
584
+ }
585
+
490
586
  /**
491
587
  * Extract an application given payload (content of the application) or package (npm-compatible identifier to the application).
492
588
  *
@@ -497,7 +593,10 @@ export const GIT_CREDENTIAL_HELPER_PATH = join(__dirname, 'gitCredentialHelper.j
497
593
  * This method may be called from any Harper thread. Same-component calls are serialized across
498
594
  * threads by the preparation lock below.
499
595
  */
500
- export async function extractApplication(application: Application) {
596
+ export async function extractApplication(
597
+ application: Application,
598
+ deferCommit = false
599
+ ): Promise<ExtractionTransaction | undefined> {
501
600
  // Can't specify neither
502
601
  if (!application.payload && !application.packageIdentifier) {
503
602
  throw new Error('Either payload or package must be provided');
@@ -507,7 +606,6 @@ export async function extractApplication(application: Application) {
507
606
  if (application.payload && application.packageIdentifier) {
508
607
  throw new Error('Both payload and package cannot be provided');
509
608
  }
510
-
511
609
  // Resolve the tarball from the input
512
610
  let tarballPath: string;
513
611
  let tarball: Readable;
@@ -576,6 +674,7 @@ export async function extractApplication(application: Application) {
576
674
  // not have, so this gates the pack step regardless of whether a credential happens to be in
577
675
  // play.
578
676
  const allowScripts = !!application.install?.allowInstallScripts;
677
+ if (allowScripts) application.installationIsOpaque = true;
579
678
  // `--ignore-scripts` alone isn't a reliable way to enforce that: pacote's DirFetcher runs a
580
679
  // git source's `prepare` unconditionally on npm versions before 11.0.0 (see
581
680
  // packGitReferenceWithoutScripts), which is exactly what Node 22's bundled npm ships. For a
@@ -630,12 +729,13 @@ export async function extractApplication(application: Application) {
630
729
  // component, and the per-component path means a sibling component never collides
631
730
  // with (or sweeps) another's aside.
632
731
  const asideStagingDir = join(dirname(application.dirPath), ASIDE_STAGING_DIR, basename(application.dirPath));
633
- let didRenameAside = false;
732
+ let asidePath: string | undefined;
634
733
  try {
635
734
  await access(application.dirPath, constants.F_OK);
636
735
  await mkdir(asideStagingDir, { recursive: true });
637
- await rename(application.dirPath, join(asideStagingDir, `${process.pid}-${Date.now()}-${randomUUID()}`));
638
- didRenameAside = true;
736
+ const candidateAsidePath = join(asideStagingDir, `${process.pid}-${Date.now()}-${randomUUID()}`);
737
+ await rename(application.dirPath, candidateAsidePath);
738
+ asidePath = candidateAsidePath;
639
739
  } catch (err) {
640
740
  // Ignore does not exist error
641
741
  if (err.code !== 'ENOENT') {
@@ -644,35 +744,37 @@ export async function extractApplication(application: Application) {
644
744
  }
645
745
  // A directory existed for this component name prior to this deploy, so this is a redeploy of
646
746
  // an already-active component rather than a first-time deploy. See `isNewComponent` above.
647
- if (didRenameAside) application.isNewComponent = false;
648
- // Finally, create the application directory fresh
649
- await mkdir(application.dirPath, { recursive: true });
650
-
651
- // Now pipeline the tarball into maybe-gunzip then tar-fs to reliably decompress and extract the contents
652
- await pipeline(tarball, gunzip(), extract(application.dirPath));
653
-
654
- // If the extracted directory contains a single folder, move the contents up one level
655
- // The `npm pack` command does this (the top-level folder is called "package")
656
- // Other packing tools may have similar behavior, but the directory name is not guaranteed.
657
- const extracted = await readdir(application.dirPath, { withFileTypes: true });
658
- if (extracted.length === 1 && extracted[0].isDirectory()) {
659
- const topLevelDirPath = join(application.dirPath, extracted[0].name);
660
-
661
- const tempDirPath = await mkdtemp(application.dirPath);
662
-
663
- // Copy contents of top-level directory to temp directory (in order to avoid collisions of top-level directory name and one of the contents)
664
- await cp(topLevelDirPath, tempDirPath, { recursive: true });
665
- // Remove top-level directory
666
- await rm(topLevelDirPath, { recursive: true, force: true });
667
- // Copy contents of temp directory to application directory
668
- await cp(tempDirPath, application.dirPath, { recursive: true });
669
- // Finally, remove the temp dir
670
- await rm(tempDirPath, { recursive: true, force: true });
747
+ if (asidePath) application.isNewComponent = false;
748
+ try {
749
+ await mkdir(application.dirPath, { recursive: true });
750
+ await pipeline(tarball, gunzip(), extract(application.dirPath));
751
+
752
+ const extracted = await readdir(application.dirPath, { withFileTypes: true });
753
+ if (extracted.length === 1 && extracted[0].isDirectory()) {
754
+ const topLevelDirPath = join(application.dirPath, extracted[0].name);
755
+ await mkdir(asideStagingDir, { recursive: true });
756
+ const tempDirPath = await mkdtemp(join(asideStagingDir, '.normalize-'));
757
+ await cp(topLevelDirPath, tempDirPath, { recursive: true });
758
+ await rm(topLevelDirPath, { recursive: true, force: true });
759
+ await cp(tempDirPath, application.dirPath, { recursive: true });
760
+ await rm(tempDirPath, { recursive: true, force: true });
761
+ }
762
+ } catch (error) {
763
+ try {
764
+ await rollbackExtractedDirectory(application, asideStagingDir, asidePath);
765
+ } catch (rollbackError) {
766
+ throw new AggregateError(
767
+ [error, rollbackError],
768
+ `Failed to extract ${application.name} and restore its previous component directory`
769
+ );
770
+ }
771
+ throw error;
671
772
  }
672
-
673
773
  // Clean up the original tarball
674
774
  if (shouldDeleteTarball && tarballPath) {
675
- await rm(tarballPath, { force: true });
775
+ await rm(tarballPath, { force: true }).catch((error) =>
776
+ application.logger.warn(`Failed to remove temporary package ${tarballPath}:`, error)
777
+ );
676
778
  }
677
779
 
678
780
  // Remove this component's aside copies. The old worker may still hold files open
@@ -681,11 +783,76 @@ export async function extractApplication(application: Application) {
681
783
  // earlier deploys whose workers have since exited, and a copy that survives because
682
784
  // its worker is still live is swept by the next deploy. The failure is expected in
683
785
  // the live-worker case, so it's logged at trace rather than as a warning.
684
- if (didRenameAside) {
685
- rm(asideStagingDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 }).catch((err) =>
686
- logger.trace?.(`Deferred cleanup of previous ${application.name} component directory: ${err.message}`)
687
- );
786
+ let settled = false;
787
+ const transaction: ExtractionTransaction = {
788
+ async commit() {
789
+ if (settled) return;
790
+ settled = true;
791
+ await cleanupExtractionStaging(application, asideStagingDir);
792
+ },
793
+ async rollback() {
794
+ if (settled) return;
795
+ settled = true;
796
+ await rollbackExtractedDirectory(application, asideStagingDir, asidePath);
797
+ },
798
+ };
799
+ if (deferCommit) return transaction;
800
+ await transaction.commit();
801
+ }
802
+
803
+ async function cleanupExtractionStaging(application: Application, asideStagingDir: string): Promise<void> {
804
+ try {
805
+ await rm(asideStagingDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
806
+ } catch (error) {
807
+ logger.trace?.(`Cleanup of previous ${application.name} component directory deferred: ${error.message}`);
808
+ }
809
+ }
810
+
811
+ async function rollbackExtractedDirectory(
812
+ application: Application,
813
+ asideStagingDir: string,
814
+ asidePath: string | undefined
815
+ ): Promise<void> {
816
+ await mkdir(asideStagingDir, { recursive: true });
817
+ const retryableRenameCodes = new Set(['EEXIST', 'ENOTEMPTY', 'EPERM', 'EACCES', 'EBUSY']);
818
+ const displaceCurrentDirectory = async () => {
819
+ const displacedPath = join(asideStagingDir, `.failed-${process.pid}-${Date.now()}-${randomUUID()}`);
820
+ let lastError: unknown;
821
+ for (let attempt = 0; attempt < 100; attempt++) {
822
+ try {
823
+ await rename(application.dirPath, displacedPath);
824
+ return;
825
+ } catch (error) {
826
+ const code = (error as NodeJS.ErrnoException).code;
827
+ if (code === 'ENOENT') return;
828
+ if (!retryableRenameCodes.has(code ?? '')) throw error;
829
+ lastError = error;
830
+ await delay(10);
831
+ }
832
+ }
833
+ throw lastError;
834
+ };
835
+
836
+ await displaceCurrentDirectory();
837
+ if (asidePath) {
838
+ let restored = false;
839
+ let restoreError: unknown;
840
+ for (let attempt = 0; attempt < 100; attempt++) {
841
+ try {
842
+ await rename(asidePath, application.dirPath);
843
+ restored = true;
844
+ break;
845
+ } catch (error) {
846
+ restoreError = error;
847
+ if (!retryableRenameCodes.has((error as NodeJS.ErrnoException).code ?? '')) break;
848
+ await displaceCurrentDirectory();
849
+ await delay(10);
850
+ }
851
+ }
852
+ if (!restored) throw restoreError;
688
853
  }
854
+
855
+ await cleanupExtractionStaging(application, asideStagingDir);
689
856
  }
690
857
 
691
858
  /**
@@ -711,7 +878,10 @@ export async function installApplication(application: Application) {
711
878
  try {
712
879
  // Does node_modules exist?
713
880
  await access(join(application.dirPath, 'node_modules'), constants.F_OK);
714
- application.logger.info(`Application ${application.name} already has node_modules; skipping install`);
881
+ application.logger.info(
882
+ `Application ${application.name} already has node_modules; skipping install and treating the runtime as opaque for redeploy comparison`
883
+ );
884
+ application.installationIsOpaque = true;
715
885
  return;
716
886
  } catch (err) {
717
887
  if (err.code !== 'ENOENT') throw err;
@@ -735,6 +905,7 @@ export async function installApplication(application: Application) {
735
905
  );
736
906
  // if it succeeds, return
737
907
  if (code === 0) {
908
+ application.installationIsOpaque = true;
738
909
  return;
739
910
  }
740
911
  if (stdout) {
@@ -797,6 +968,7 @@ export async function installApplication(application: Application) {
797
968
 
798
969
  // if it succeeds, return
799
970
  if (code === 0) {
971
+ if (application.install?.allowInstallScripts) application.installationIsOpaque = true;
800
972
  return;
801
973
  }
802
974
 
@@ -854,6 +1026,7 @@ export async function installApplication(application: Application) {
854
1026
 
855
1027
  // if it succeeds, return
856
1028
  if (code === 0) {
1029
+ if (application.install?.allowInstallScripts) application.installationIsOpaque = true;
857
1030
  return;
858
1031
  }
859
1032
 
@@ -911,14 +1084,10 @@ export class Application {
911
1084
  npmUserconfigPath?: string;
912
1085
  #npmrcTempDir?: string;
913
1086
  #gitCredentialSession?: GitCredentialSession;
914
- // Whether this component's directory did not already exist when extractApplication ran
915
- // i.e. this deploy is the component's first, as opposed to a redeploy of something already
916
- // active. Defaults true and is flipped to false by extractApplication when it finds (and
917
- // renames aside) a pre-existing directory for this component name. Used by deployComponent to
918
- // scope its unconditional requestRestart() call to genuinely new components (harper#1806):
919
- // an existing, already-loaded component already has a live file watcher (Scope/EntryHandler)
920
- // that independently requests a restart if the redeploy actually needs one.
1087
+ // Existing components rely on their runtime-equivalence checks; only a first deploy restarts unconditionally.
921
1088
  isNewComponent: boolean = true;
1089
+ packageMetadataChanged: boolean = false;
1090
+ installationIsOpaque: boolean = false;
922
1091
 
923
1092
  constructor({ name, payload, packageIdentifier, install, onInstallLine, credentials }: ApplicationOptions) {
924
1093
  this.name = name;
@@ -1064,12 +1233,14 @@ export function derivePackageIdentifier(packageIdentifier: string) {
1064
1233
  * @returns A promise that resolves when all preparation steps complete.
1065
1234
  */
1066
1235
  export async function prepareApplication(application: Application) {
1067
- await broadcastDeployStart(application.name);
1236
+ const deploymentId = await broadcastDeployStart(application.name);
1068
1237
  try {
1069
1238
  const commandTimeoutMs = application.install?.timeout ?? DEFAULT_COMMAND_TIMEOUT_MS;
1070
1239
  await withComponentPreparationLock(
1071
1240
  application.dirPath,
1072
1241
  async () => {
1242
+ const previousPackageMetadata = await readInstalledPackageMetadata(application.dirPath);
1243
+ let extraction: ExtractionTransaction | undefined;
1073
1244
  try {
1074
1245
  // Materialize the per-deploy `.npmrc` before extraction so both `npm pack` (extract) and
1075
1246
  // `npm install` authenticate against the private registry; always remove it afterward.
@@ -1080,11 +1251,30 @@ export async function prepareApplication(application: Application) {
1080
1251
  // already gone by the time the component's dependency tree (and any install script it is
1081
1252
  // allowed to run) executes.
1082
1253
  await application.startGitCredentialSession();
1083
- await extractApplication(application);
1254
+ extraction = await extractApplication(application, true);
1084
1255
  } finally {
1085
1256
  await application.cleanupGitCredentialSession();
1086
1257
  }
1087
1258
  await installApplication(application);
1259
+ if (!application.isNewComponent) {
1260
+ const currentPackageMetadata = await readInstalledPackageMetadata(application.dirPath);
1261
+ application.packageMetadataChanged = installedRuntimeChanged(
1262
+ previousPackageMetadata,
1263
+ currentPackageMetadata,
1264
+ application.installationIsOpaque
1265
+ );
1266
+ }
1267
+ await extraction?.commit();
1268
+ } catch (error) {
1269
+ try {
1270
+ await extraction?.rollback();
1271
+ } catch (rollbackError) {
1272
+ throw new AggregateError(
1273
+ [error, rollbackError],
1274
+ `Failed to prepare ${application.name} and restore its previous component directory`
1275
+ );
1276
+ }
1277
+ throw error;
1088
1278
  } finally {
1089
1279
  await application.cleanupTransientNpmrc();
1090
1280
  }
@@ -1111,7 +1301,7 @@ export async function prepareApplication(application: Application) {
1111
1301
  }
1112
1302
  );
1113
1303
  } finally {
1114
- broadcastDeployEnd(application.name);
1304
+ broadcastDeployEnd(application.name, deploymentId);
1115
1305
  }
1116
1306
  }
1117
1307
 
@@ -4,6 +4,8 @@ import { forComponent } from '../utility/logging/harper_logger.ts';
4
4
  import { scopedImport } from '../security/jsLoader.ts';
5
5
  import * as env from '../utility/environment/environmentManager.ts';
6
6
  import { CONFIG_PARAMS } from '../utility/hdbTerms.ts';
7
+ import { RuntimeModuleTracker } from './RuntimeModuleTracker.ts';
8
+ import { deployLifecycle } from './deployLifecycle.ts';
7
9
 
8
10
  export class MissingDefaultFilesOptionError extends Error {
9
11
  constructor() {
@@ -24,14 +26,18 @@ export class ApplicationScope {
24
26
  mode?: 'native' | 'vm' | 'vm-current-context' | 'compartment'; // option to set this from the scope
25
27
  dependencyLoader?: 'native' | 'app' | 'auto'; // option to set this from the scope
26
28
  allowedPath?: string;
29
+ runtimeRoot?: string;
27
30
  config: any;
28
31
  moduleCache: any; // used by the loader to retain a cache of modules, type is an internal detail of the loader
32
+ #runtimeModules: RuntimeModuleTracker;
29
33
  constructor(name: string, resources: Resources, server: Server, isInternal = false) {
30
34
  this.name = name;
31
35
  this.logger = forComponent(name, !isInternal);
32
36
 
33
37
  this.resources = resources;
34
38
  this.server = server;
39
+ this.#runtimeModules = new RuntimeModuleTracker(() => this.runtimeRoot);
40
+ if (deployLifecycle.isDeployInFlight(name)) this.#runtimeModules.beginDeploy();
35
41
 
36
42
  this.mode = env.get(CONFIG_PARAMS.APPLICATIONS_MODULELOADER) ?? 'vm-current-context';
37
43
  this.dependencyLoader = env.get(CONFIG_PARAMS.APPLICATIONS_DEPENDENCYLOADER);
@@ -50,4 +56,24 @@ export class ApplicationScope {
50
56
  async import(filePath: string): Promise<unknown> {
51
57
  return scopedImport(filePath, this);
52
58
  }
59
+
60
+ recordLoadedModule(moduleUrl: string, source: string | Buffer): void {
61
+ this.#runtimeModules.recordModule(moduleUrl, source);
62
+ }
63
+
64
+ recordModuleResolution(specifier: string, referrer: string, resolvedUrl: string): void {
65
+ this.#runtimeModules.recordResolution(specifier, referrer, resolvedUrl);
66
+ }
67
+
68
+ markNativeRuntime(): void {
69
+ this.#runtimeModules.markNativeRuntime();
70
+ }
71
+
72
+ beginDeploy(): void {
73
+ this.#runtimeModules.beginDeploy();
74
+ }
75
+
76
+ finishDeploy(): Promise<boolean> {
77
+ return this.#runtimeModules.finishDeploy();
78
+ }
53
79
  }