@lowdefy/server-dev 6.0.0 → 7.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (625) hide show
  1. package/client/App.jsx +52 -24
  2. package/client/Inspector.jsx +73 -30
  3. package/client/JourneyObserver.jsx +47 -0
  4. package/client/Page.jsx +54 -22
  5. package/client/Recorder.jsx +73 -0
  6. package/client/Routing.jsx +37 -13
  7. package/client/clientBuildImports.test.mjs +63 -0
  8. package/client/devStreamConnections.test.mjs +40 -0
  9. package/client/feedback/FeedbackMount.jsx +2 -2
  10. package/client/feedback/FeedbackOverlay.jsx +7 -4
  11. package/client/feedback/feedbackStyles.js +3 -0
  12. package/client/journeyObserver/startJourneyObserver.js +101 -0
  13. package/client/journeyObserver/startJourneyObserver.test.mjs +149 -0
  14. package/client/openInEditor/OpenInEditorListener.jsx +2 -0
  15. package/client/recorder/buildEngineRecord.js +37 -0
  16. package/client/recorder/buildInteractionRecord.js +64 -0
  17. package/client/recorder/buildTraceEvent.js +57 -0
  18. package/client/recorder/buildTraceEvent.test.mjs +81 -0
  19. package/client/recorder/chunkRecords.js +69 -0
  20. package/client/recorder/chunkRecords.test.mjs +59 -0
  21. package/client/recorder/collectEventActions.js +30 -0
  22. package/client/recorder/collectEventEndpoints.js +40 -0
  23. package/client/recorder/collectEventRequests.js +39 -0
  24. package/client/recorder/createPairingBuffer.js +187 -0
  25. package/client/recorder/createPairingBuffer.test.mjs +405 -0
  26. package/client/recorder/createPasswordRedactor.js +84 -0
  27. package/client/recorder/createRecordBatcher.js +54 -0
  28. package/client/recorder/createRecordBatcher.test.mjs +76 -0
  29. package/client/recorder/createRecorder.js +216 -0
  30. package/client/recorder/createRecorder.test.mjs +318 -0
  31. package/client/recorder/createRecordingSession.js +61 -0
  32. package/client/recorder/createRecordingSession.test.mjs +72 -0
  33. package/client/recorder/diffStateWrites.js +77 -0
  34. package/client/recorder/diffStateWrites.test.mjs +66 -0
  35. package/client/recorder/isEventCall.js +30 -0
  36. package/client/recorder/jsonType.js +39 -0
  37. package/client/recorder/keyChord.js +47 -0
  38. package/client/recorder/keyChord.test.mjs +50 -0
  39. package/client/recorder/sendRecords.js +40 -0
  40. package/client/recorder/toJsonSafe.js +29 -0
  41. package/lib/client/BuildingPage.jsx +3 -1
  42. package/lib/client/ErrorBar.jsx +17 -13
  43. package/lib/client/RedirectingPage.jsx +56 -0
  44. package/lib/client/auth/Auth.jsx +11 -6
  45. package/lib/client/auth/AuthConfigured.jsx +224 -39
  46. package/lib/client/auth/AuthNotConfigured.js +45 -4
  47. package/lib/client/auth/authNotConfiguredParity.test.mjs +61 -0
  48. package/lib/client/auth/authTemplateSync.test.mjs +40 -0
  49. package/lib/client/auth/betterAuthSessionContract.test.mjs +181 -0
  50. package/lib/client/auth/createFetchCheckingBuild.js +45 -0
  51. package/lib/client/bundleBuildId.js +25 -0
  52. package/lib/client/collectVisibleBlockIds.js +51 -0
  53. package/lib/client/getCspNonce.js +25 -0
  54. package/lib/client/getPagePath.js +36 -0
  55. package/lib/client/getPagePath.test.mjs +51 -0
  56. package/lib/client/utils/formatErrorsForCopy.js +30 -0
  57. package/lib/client/utils/formatErrorsForCopy.test.mjs +39 -0
  58. package/lib/client/utils/getErrorBarColor.js +25 -0
  59. package/lib/client/utils/getErrorBarColor.test.mjs +34 -0
  60. package/lib/client/utils/request.js +8 -1
  61. package/lib/client/utils/usePageConfig.js +89 -25
  62. package/lib/client/utils/usePageConfig.test.mjs +198 -13
  63. package/lib/configTree/buildConfigTree.mjs +58 -0
  64. package/lib/configTree/buildConfigTree.test.mjs +77 -0
  65. package/lib/configTree/collectConfigText.mjs +101 -0
  66. package/lib/configTree/collectConfigText.test.mjs +116 -0
  67. package/lib/configTree/prepareScratchServer.mjs +46 -0
  68. package/lib/configTree/runConfigTreeBuild.mjs +88 -0
  69. package/lib/configTree/runConfigTreeBuild.skipPluginChecks.test.mjs +74 -0
  70. package/lib/configTree/runConfigTreeBuild.test.mjs +202 -0
  71. package/lib/docs/JourneyStepError.js +30 -0
  72. package/lib/docs/acquireBrowserSlot.js +30 -0
  73. package/lib/docs/addNestedEndpoints.js +54 -0
  74. package/lib/docs/addNestedEndpoints.test.mjs +185 -0
  75. package/lib/docs/addWriteFlags.js +77 -0
  76. package/lib/docs/buildEditedPages.js +50 -0
  77. package/lib/docs/buildWaitHeader.js +21 -0
  78. package/lib/docs/captureAnnotatedScreenshot.js +23 -12
  79. package/lib/docs/checkAgentFiles.js +148 -0
  80. package/lib/docs/checkAgentFiles.test.mjs +225 -0
  81. package/lib/docs/checkStepReferences.js +239 -0
  82. package/lib/docs/checkStepReferences.test.mjs +173 -0
  83. package/lib/docs/checkpointStore.js +3 -0
  84. package/lib/docs/clientErrorStore.js +18 -1
  85. package/lib/docs/clientErrorStore.test.mjs +46 -0
  86. package/lib/docs/collectExercised.js +52 -0
  87. package/lib/docs/createBrowserLifecycle.js +102 -0
  88. package/lib/docs/createBrowserLifecycle.test.mjs +212 -0
  89. package/lib/docs/createDocsMcpServer.js +345 -388
  90. package/lib/docs/createDocsMcpServer.test.mjs +644 -4
  91. package/lib/docs/createDocsMcpServerRunJourney.test.mjs +148 -0
  92. package/lib/docs/createJourneyActors.chromium.test.mjs +227 -0
  93. package/lib/docs/createJourneyActors.js +213 -0
  94. package/lib/docs/createJourneyActors.test.mjs +79 -0
  95. package/lib/docs/createJourneyObservations.js +80 -0
  96. package/lib/docs/createLeftOriginError.js +26 -0
  97. package/lib/docs/createNetworkCounter.js +101 -0
  98. package/lib/docs/dataSets/checkSharedCollectionIds.js +67 -0
  99. package/lib/docs/dataSets/dataSessionRegistry.js +25 -0
  100. package/lib/docs/dataSets/dataSetJourneys.chromium.mongodb.test.mjs +679 -0
  101. package/lib/docs/dataSets/describeDataSetResult.js +25 -0
  102. package/lib/docs/dataSets/describeDatabaseTarget.js +46 -0
  103. package/lib/docs/dataSets/deserializeFixture.js +34 -0
  104. package/lib/docs/dataSets/drainSessionWork.js +37 -0
  105. package/lib/docs/dataSets/drainSessionWork.test.mjs +64 -0
  106. package/lib/docs/dataSets/duplicateIndexName.js +24 -0
  107. package/lib/docs/dataSets/findCollectionCollisions.js +52 -0
  108. package/lib/docs/dataSets/getDataStore.js +106 -0
  109. package/lib/docs/dataSets/getDataStore.mongodb.test.mjs +63 -0
  110. package/lib/docs/dataSets/getDataStore.test.mjs +195 -0
  111. package/lib/docs/dataSets/groupDataSetByCollection.js +44 -0
  112. package/lib/docs/dataSets/insertFixture.js +57 -0
  113. package/lib/docs/dataSets/insertGeneratedDocuments.js +41 -0
  114. package/lib/docs/dataSets/isDataStoreGone.js +29 -0
  115. package/lib/docs/dataSets/loadDataSetCollection.js +42 -0
  116. package/lib/docs/dataSets/mergeCollectionIndexes.js +52 -0
  117. package/lib/docs/dataSets/mergeCollectionIndexes.test.mjs +59 -0
  118. package/lib/docs/dataSets/openDataSession.js +119 -0
  119. package/lib/docs/dataSets/openDataSession.mongodb.test.mjs +343 -0
  120. package/lib/docs/dataSets/pickDataStorePort.js +58 -0
  121. package/lib/docs/dataSets/pickDataStorePort.test.mjs +63 -0
  122. package/lib/docs/dataSets/readConnectionArtifacts.js +46 -0
  123. package/lib/docs/dataSets/readDataSession.js +41 -0
  124. package/lib/docs/dataSets/readDataSession.test.mjs +57 -0
  125. package/lib/docs/dataSets/readDataSet.js +68 -0
  126. package/lib/docs/dataSets/readDataSet.test.mjs +220 -0
  127. package/lib/docs/dataSets/registerDataStoreShutdown.js +47 -0
  128. package/lib/docs/dataSets/removeDataStoreFiles.js +32 -0
  129. package/lib/docs/dataSets/resolveDataSetCollection.js +46 -0
  130. package/lib/docs/dataSets/resolveJourneyDataSet.js +106 -0
  131. package/lib/docs/dataSets/resolveJourneyDataSet.test.mjs +130 -0
  132. package/lib/docs/dataSets/sweepOrphanDatabases.js +33 -0
  133. package/lib/docs/dataSets/test/app/lowdefy.yaml +203 -0
  134. package/lib/docs/dataSets/test/app/modules/archive/module.lowdefy.yaml +30 -0
  135. package/lib/docs/dataSets/test/app/tests/data/shop.yaml +8 -0
  136. package/lib/docs/dataSets/test/buildTestApp.mjs +43 -0
  137. package/lib/docs/devEventBus.js +136 -0
  138. package/lib/docs/devEventBus.test.mjs +269 -0
  139. package/lib/docs/devToolDefinitions.js +529 -0
  140. package/lib/docs/docSources.js +21 -0
  141. package/lib/docs/docs.test.mjs +171 -15
  142. package/lib/docs/docsIndex.test.mjs +377 -0
  143. package/lib/docs/evalOperator.js +5 -3
  144. package/lib/docs/evalOperatorHeadless.js +84 -12
  145. package/lib/docs/evalOperatorHeadless.test.mjs +7 -1
  146. package/lib/docs/evalOperatorInTab.js +2 -1
  147. package/lib/docs/findConfig.js +95 -49
  148. package/lib/docs/fitResponse.js +81 -0
  149. package/lib/docs/fitResponse.test.mjs +128 -0
  150. package/lib/docs/formatFeedback.js +26 -5
  151. package/lib/docs/formatFeedback.test.mjs +48 -0
  152. package/lib/docs/getAppMap.js +7 -1
  153. package/lib/docs/getBrowser.js +150 -80
  154. package/lib/docs/getBrowser.test.mjs +407 -16
  155. package/lib/docs/getBuildId.js +61 -0
  156. package/lib/docs/getBuildId.test.mjs +84 -0
  157. package/lib/docs/getBuildStatus.js +54 -6
  158. package/lib/docs/getBuildStatus.test.mjs +133 -0
  159. package/lib/docs/getBuildStatusAfterEdits.js +35 -0
  160. package/lib/docs/getBuildStatusAfterEdits.test.mjs +76 -0
  161. package/lib/docs/getCoreDoc.js +9 -1
  162. package/lib/docs/getDoc.js +100 -0
  163. package/lib/docs/getDocsIndex.js +152 -0
  164. package/lib/docs/getHazards.js +115 -0
  165. package/lib/docs/getHazards.test.mjs +175 -0
  166. package/lib/docs/getMailOutboxDirectory.js +26 -0
  167. package/lib/docs/getOverview.js +79 -13
  168. package/lib/docs/getOverviewBasePath.test.mjs +37 -0
  169. package/lib/docs/getPageBuildStatus.js +46 -0
  170. package/lib/docs/getPluginDoc.js +21 -23
  171. package/lib/docs/getSchema.js +33 -1
  172. package/lib/docs/getStaleStatus.js +36 -0
  173. package/lib/docs/getStaleStatus.test.mjs +74 -0
  174. package/lib/docs/guardJourneyOrigin.js +39 -0
  175. package/lib/docs/inspectState.js +5 -3
  176. package/lib/docs/inspectStateFromTab.js +2 -2
  177. package/lib/docs/inspectStateHeadless.js +89 -27
  178. package/lib/docs/inspectStateHeadless.test.mjs +20 -1
  179. package/lib/docs/installHeadlessShell.failure.test.mjs +45 -0
  180. package/lib/docs/installHeadlessShell.js +123 -0
  181. package/lib/docs/installHeadlessShell.test.mjs +70 -0
  182. package/lib/docs/installHeadlessShell.timeout.test.mjs +72 -0
  183. package/lib/docs/isHeadlessShellIncomplete.js +34 -0
  184. package/lib/docs/isHeadlessShellIncomplete.test.mjs +63 -0
  185. package/lib/docs/isPageReady.js +77 -0
  186. package/lib/docs/isPageReady.test.mjs +235 -0
  187. package/lib/docs/isRunErrorEntry.js +25 -0
  188. package/lib/docs/journeyMutantCookie.chromium.test.mjs +150 -0
  189. package/lib/docs/journeyRunRecording.js +35 -0
  190. package/lib/docs/journeyRunRecording.test.mjs +39 -0
  191. package/lib/docs/keepEventStreamAlive.js +68 -0
  192. package/lib/docs/keepEventStreamAlive.test.mjs +77 -0
  193. package/lib/docs/launchBrowser.js +83 -0
  194. package/lib/docs/launchBrowser.test.mjs +175 -0
  195. package/lib/docs/listModuleDocEntries.js +69 -0
  196. package/lib/docs/listPackageDocFiles.js +67 -0
  197. package/lib/docs/listPluginDocEntries.js +111 -0
  198. package/lib/docs/listPlugins.js +9 -58
  199. package/lib/docs/listTypes.js +14 -6
  200. package/lib/docs/loadFrameworkHazards.js +40 -0
  201. package/lib/docs/loadFrameworkHazards.test.mjs +43 -0
  202. package/lib/docs/loadState.js +103 -35
  203. package/lib/docs/makeModuleManifestDoc.js +174 -0
  204. package/lib/docs/matchExercisedPages.js +35 -0
  205. package/lib/docs/matchJourneyRoute.js +69 -0
  206. package/lib/docs/matchJourneyRoute.test.mjs +178 -0
  207. package/lib/docs/mergeNetworkSnapshots.js +49 -0
  208. package/lib/docs/nextJourneySequence.js +30 -0
  209. package/lib/docs/noBrowserError.js +22 -0
  210. package/lib/docs/observe/TRANSIENT_SELECTOR.js +21 -0
  211. package/lib/docs/observe/claimExpectedErrors.js +67 -0
  212. package/lib/docs/observe/createJourneyAppErrors.js +236 -0
  213. package/lib/docs/observe/createJourneyAppErrors.test.mjs +385 -0
  214. package/lib/docs/observe/evaluateInvariants.js +183 -0
  215. package/lib/docs/observe/evaluateInvariants.test.mjs +299 -0
  216. package/lib/docs/observe/findingKey.js +29 -0
  217. package/lib/docs/observe/hasNoEffect.js +31 -0
  218. package/lib/docs/observe/hasNoEffect.test.mjs +37 -0
  219. package/lib/docs/observe/installStepObserver.js +72 -0
  220. package/lib/docs/observe/isAppApiUrl.js +33 -0
  221. package/lib/docs/observe/parseAppApiUrl.js +37 -0
  222. package/lib/docs/observe/readStepObserver.js +42 -0
  223. package/lib/docs/observe/relativeSource.js +29 -0
  224. package/lib/docs/observe/resolveConfigKeySource.js +33 -0
  225. package/lib/docs/observe/takeJourneyEvents.js +36 -0
  226. package/lib/docs/observe/toAppErrorFailure.js +61 -0
  227. package/lib/docs/observe/usesSearchStage.js +45 -0
  228. package/lib/docs/observe/waitForClientErrorReports.js +31 -0
  229. package/lib/docs/observe/watchJourneyContext.js +75 -0
  230. package/lib/docs/observe/withAppErrors.js +31 -0
  231. package/lib/docs/observe/withAppErrors.test.mjs +101 -0
  232. package/lib/docs/openJourney.js +94 -0
  233. package/lib/docs/openJourneyEmail.js +41 -0
  234. package/lib/docs/openJourneyEmail.test.mjs +187 -0
  235. package/lib/docs/pageBlocks.js +47 -0
  236. package/lib/docs/pageBuildStatus.test.mjs +103 -0
  237. package/lib/{client/setPageId.js → docs/parseModuleSource.js} +11 -12
  238. package/lib/docs/rails/authEngineActions.js +53 -0
  239. package/lib/docs/rails/authEngineActions.test.mjs +69 -0
  240. package/lib/docs/rails/authEngineMethods.js +52 -0
  241. package/lib/docs/rails/authEngineMethods.test.mjs +36 -0
  242. package/lib/docs/rails/checkAuthActionsRan.js +66 -0
  243. package/lib/docs/rails/checkAuthActionsRan.test.mjs +149 -0
  244. package/lib/docs/rails/checkClickRails.js +56 -0
  245. package/lib/docs/rails/createDataSetRails.js +82 -0
  246. package/lib/docs/rails/createDataSetRails.test.mjs +172 -0
  247. package/lib/docs/rails/findExternalBlocks.js +147 -0
  248. package/lib/docs/rails/findExternalBlocks.test.mjs +254 -0
  249. package/lib/docs/rails/installAuthActionRecorder.js +56 -0
  250. package/lib/docs/rails/readAuthActionRecorder.js +32 -0
  251. package/lib/docs/rails/readBuiltArtifact.js +30 -0
  252. package/lib/docs/readConnectionPlugins.js +26 -0
  253. package/lib/docs/readDevAuthMode.js +32 -0
  254. package/lib/docs/readDocEntry.js +40 -0
  255. package/lib/docs/readDocTitle.js +26 -0
  256. package/lib/docs/readJourneyEmailMatch.js +57 -0
  257. package/lib/docs/readJourneyEmailMatch.test.mjs +110 -0
  258. package/lib/docs/readJourneySession.js +56 -0
  259. package/lib/docs/readMailOutbox.js +37 -0
  260. package/lib/docs/readPagePath.js +27 -0
  261. package/lib/docs/readPluginPackages.js +99 -0
  262. package/lib/docs/readProxyBuildWait.js +38 -0
  263. package/lib/docs/readRoutineSteps.js +45 -0
  264. package/lib/docs/registerShownTextEngine.js +118 -0
  265. package/lib/docs/requestRestart.js +30 -0
  266. package/lib/docs/requestRestart.test.mjs +49 -0
  267. package/lib/docs/resolvePageInstance.js +46 -0
  268. package/lib/docs/resolvePageInstance.test.mjs +81 -0
  269. package/lib/docs/resolveSource.js +11 -7
  270. package/lib/docs/resolveSource.test.mjs +1 -1
  271. package/lib/docs/resolveToolCaller.js +62 -0
  272. package/lib/docs/reviewPage.js +51 -0
  273. package/lib/docs/reviewPageBuilds.js +56 -0
  274. package/lib/docs/reviewPageBuilds.test.mjs +207 -0
  275. package/lib/docs/runCheck.js +75 -0
  276. package/lib/docs/runCheck.test.mjs +91 -0
  277. package/lib/docs/runCheckChild.js +49 -0
  278. package/lib/docs/runEndpoint.js +185 -0
  279. package/lib/docs/runEndpoint.test.mjs +509 -0
  280. package/lib/docs/runErrorBuffers.js +99 -0
  281. package/lib/docs/runErrorBuffers.test.mjs +95 -0
  282. package/lib/docs/runJourney.js +347 -0
  283. package/lib/docs/runJourney.test.mjs +2383 -0
  284. package/lib/docs/runJourneyAsUsers.js +44 -0
  285. package/lib/docs/runJourneyDataSet.test.mjs +274 -0
  286. package/lib/docs/runJourneySteps.js +1320 -0
  287. package/lib/docs/runRequest.js +100 -59
  288. package/lib/docs/runRequest.test.mjs +190 -0
  289. package/lib/docs/saveAnnotatedScreenshot.js +1 -1
  290. package/lib/docs/scaffoldPage.js +3 -1
  291. package/lib/docs/screenshotPage.js +163 -11
  292. package/lib/docs/screenshotPage.test.mjs +263 -1
  293. package/lib/docs/screenshotPageClip.test.mjs +4 -0
  294. package/lib/docs/searchDocs.js +82 -30
  295. package/lib/docs/searchDocs.test.mjs +100 -0
  296. package/lib/docs/searchIcons.js +90 -0
  297. package/lib/docs/searchIcons.test.mjs +127 -0
  298. package/lib/docs/selectExercised.js +39 -0
  299. package/lib/docs/selectFinalState.js +66 -0
  300. package/lib/docs/selectFinalState.test.mjs +51 -0
  301. package/lib/docs/serverErrorStore.js +54 -0
  302. package/lib/docs/serverErrorStore.test.mjs +50 -0
  303. package/lib/docs/setupTestFixtures.mjs +133 -39
  304. package/lib/docs/shownTextSelector.js +28 -0
  305. package/lib/docs/snapshotState.js +13 -5
  306. package/lib/docs/splitSearchTerms.js +62 -0
  307. package/lib/docs/splitSearchTerms.test.mjs +50 -0
  308. package/lib/docs/stateCheckpoints.test.mjs +77 -0
  309. package/lib/docs/tabAvailable.js +6 -5
  310. package/lib/docs/tabChannel.js +127 -21
  311. package/lib/docs/tabChannel.test.mjs +187 -29
  312. package/lib/docs/unsettledPageNote.js +24 -0
  313. package/lib/docs/validateJourneyMail.js +36 -0
  314. package/lib/docs/validateJourneyTimeout.js +38 -0
  315. package/lib/docs/validateJourneyTimeout.test.mjs +29 -0
  316. package/lib/docs/validateRecordingParam.js +42 -0
  317. package/lib/docs/validateStateSelection.js +33 -0
  318. package/lib/docs/validateStateSelection.test.mjs +33 -0
  319. package/lib/docs/validateViewport.js +51 -0
  320. package/lib/docs/validateViewport.test.mjs +54 -0
  321. package/lib/docs/waitForBuild.js +50 -0
  322. package/lib/docs/waitForBuild.test.mjs +75 -0
  323. package/lib/docs/waitForJourneyEmail.js +77 -0
  324. package/lib/docs/withBrowserSlot.js +45 -0
  325. package/lib/docs/withBrowserSlot.test.mjs +75 -0
  326. package/lib/docs/withDataSession.js +53 -0
  327. package/lib/server/applyDataSetRedirect.js +83 -0
  328. package/lib/server/applyDataSetRedirect.test.mjs +340 -0
  329. package/lib/server/applyDataSetRedirectTenantReach.test.mjs +204 -0
  330. package/lib/server/applyDataSetRedirectWalled.test.mjs +131 -0
  331. package/lib/server/auth/createSystemContext.js +73 -0
  332. package/lib/server/auth/getAuth.js +46 -0
  333. package/lib/server/auth/getHeadlessUser.js +9 -8
  334. package/lib/server/auth/getHeadlessUser.test.mjs +41 -0
  335. package/lib/server/auth/getMockUser.js +18 -14
  336. package/lib/server/auth/{getAuthConfig.js → getStrategies.js} +12 -10
  337. package/lib/server/auth/headlessUser.js +3 -2
  338. package/lib/server/auth/journeyActor.js +35 -0
  339. package/lib/server/auth/resolveHeadlessUser.test.mjs +0 -4
  340. package/lib/server/checkPageRecord.js +44 -0
  341. package/lib/server/contextMapBudget.js +24 -0
  342. package/lib/server/contextMapBudget.test.mjs +98 -0
  343. package/lib/server/continueJitKeys.js +28 -0
  344. package/lib/server/continueJitKeys.test.mjs +43 -0
  345. package/lib/server/createDataSessionEndedError.js +33 -0
  346. package/lib/server/createDataSetRefusal.js +26 -0
  347. package/lib/server/createLowdefyContext.js +174 -41
  348. package/lib/server/createLowdefyContext.test.mjs +437 -0
  349. package/lib/server/getClientAddress.js +51 -0
  350. package/lib/server/getClientAddress.test.mjs +56 -0
  351. package/lib/server/getPageJitEnrichment.test.mjs +29 -11
  352. package/lib/server/hashConfigContent.js +26 -0
  353. package/lib/server/importAppCode.test.mjs +57 -0
  354. package/lib/server/isRoutineStep.js +27 -0
  355. package/lib/server/jitPageBuilder.js +309 -126
  356. package/lib/server/jitPageBuilder.skipPluginChecks.test.mjs +78 -0
  357. package/lib/server/jitPageBuilder.test.mjs +218 -0
  358. package/lib/server/journeyCookies/JOURNEY_COOKIES.js +37 -0
  359. package/lib/server/journeyCookies/forwardJourneyCookies.js +46 -0
  360. package/lib/server/journeyCookies/readJourneyCookie.js +52 -0
  361. package/lib/server/journeyCookies/writeJourneyCookie.js +46 -0
  362. package/lib/server/journeyCookies.js +22 -0
  363. package/lib/server/journeyCookies.test.mjs +145 -0
  364. package/lib/server/loadDynamicJsMap.js +47 -0
  365. package/lib/server/log/createHandleError.js +38 -2
  366. package/lib/server/log/createHandleError.test.mjs +125 -2
  367. package/lib/server/log/createLogger.js +3 -0
  368. package/lib/server/log/createLogger.test.mjs +77 -0
  369. package/lib/server/log/logRequest.js +2 -3
  370. package/lib/server/mutantDataSetWrappers.test.mjs +112 -0
  371. package/lib/server/mutants/applyMutant.js +39 -0
  372. package/lib/server/mutants/artifactPatterns.js +25 -0
  373. package/lib/server/mutants/buildAnchor.js +45 -0
  374. package/lib/server/mutants/collectSharedIds.js +41 -0
  375. package/lib/server/mutants/configPathSegments.js +38 -0
  376. package/lib/server/mutants/createMutantReadConfigFile.js +42 -0
  377. package/lib/server/mutants/describeEvent.js +22 -0
  378. package/lib/server/mutants/enumerateArtifact.js +56 -0
  379. package/lib/server/mutants/findNodeByKey.js +45 -0
  380. package/lib/server/mutants/findNodeByPointer.js +49 -0
  381. package/lib/server/mutants/getArtifactKind.js +32 -0
  382. package/lib/server/mutants/groupMutantCopies.js +56 -0
  383. package/lib/server/mutants/hashArtifact.js +25 -0
  384. package/lib/server/mutants/hashMutantTarget.js +34 -0
  385. package/lib/server/mutants/listMutants.js +122 -0
  386. package/lib/server/mutants/listMutants.test.mjs +502 -0
  387. package/lib/server/mutants/mutantId.js +30 -0
  388. package/lib/server/mutants/mutantRuns.js +57 -0
  389. package/lib/server/mutants/mutantRuns.test.mjs +136 -0
  390. package/lib/server/mutants/operators/dropAction.js +47 -0
  391. package/lib/server/mutants/operators/dropBlock.js +44 -0
  392. package/lib/server/mutants/operators/dropPayload.js +52 -0
  393. package/lib/server/mutants/operators/dropStep.js +44 -0
  394. package/lib/server/mutants/operators/flipVisible.js +53 -0
  395. package/lib/server/mutants/operators/index.js +37 -0
  396. package/lib/server/mutants/operators/retargetLink.js +72 -0
  397. package/lib/server/mutants/operators/skipValidate.js +77 -0
  398. package/lib/server/mutants/operators/swapIf.js +112 -0
  399. package/lib/server/mutants/operators.test.mjs +396 -0
  400. package/lib/server/mutants/positionFreePath.js +38 -0
  401. package/lib/server/mutants/removeFromParent.js +28 -0
  402. package/lib/server/mutants/test/fixtures.mjs +173 -0
  403. package/lib/server/mutants/test/keyArtifact.mjs +63 -0
  404. package/lib/server/mutants/validateMutantParam.js +69 -0
  405. package/lib/server/mutants/validateMutantsBody.js +88 -0
  406. package/lib/server/mutants/walkArtifact.js +179 -0
  407. package/lib/server/pageBuildRecords.js +131 -0
  408. package/lib/server/pageBuildRecords.test.mjs +258 -0
  409. package/lib/server/pageCache.mjs +22 -7
  410. package/lib/server/pageCache.test.mjs +22 -7
  411. package/lib/server/pageRebuilds.test.mjs +543 -0
  412. package/lib/server/pruneJitMaps.js +36 -0
  413. package/lib/server/pruneJitMaps.test.mjs +55 -0
  414. package/lib/server/readInstanceRecord.js +33 -0
  415. package/lib/server/readInstanceRecord.test.mjs +99 -0
  416. package/lib/server/readMergedMaps.js +86 -0
  417. package/lib/server/readMergedMaps.test.mjs +79 -0
  418. package/lib/server/recording/appendRecords.js +33 -0
  419. package/lib/server/recording/errorRecordingStamp.js +34 -0
  420. package/lib/server/recording/errorRecordingStamp.test.mjs +31 -0
  421. package/lib/server/recording/isRecordingEnabled.js +31 -0
  422. package/lib/server/recording/readRecordingCookie.js +71 -0
  423. package/lib/server/recording/readRecordingCookie.test.mjs +73 -0
  424. package/lib/server/recording/recordingCookiePayload.js +33 -0
  425. package/lib/server/recording/servedBuilds.js +46 -0
  426. package/lib/server/recording/servedBuilds.test.mjs +42 -0
  427. package/lib/server/scrubSecrets.js +23 -0
  428. package/lib/server/sinkExternalConnections.js +95 -0
  429. package/lib/server/sinkExternalConnections.test.mjs +135 -0
  430. package/lib/server/skipStaleMapWrites.js +62 -0
  431. package/lib/server/skipStaleMapWrites.test.mjs +113 -0
  432. package/lib/server/startIdleGc.js +67 -0
  433. package/lib/server/startIdleGc.test.mjs +138 -0
  434. package/lib/server/testFixtures/importAppCodeTwice.mjs +70 -0
  435. package/lib/server/trackSessionWork.js +30 -0
  436. package/lib/vite/createDevServerExclude.js +40 -0
  437. package/lib/vite/createDevServerExclude.test.mjs +79 -0
  438. package/manager/getContext.mjs +24 -2
  439. package/manager/processes/initialBuild.mjs +14 -0
  440. package/manager/processes/initialBuild.test.mjs +101 -0
  441. package/manager/processes/lowdefyBuild.mjs +30 -7
  442. package/manager/processes/lowdefyBuild.skipPluginChecks.test.mjs +62 -0
  443. package/manager/processes/lowdefyBuild.test.mjs +136 -1
  444. package/manager/processes/optimizeDependencies.mjs +59 -0
  445. package/manager/processes/restartServer.mjs +34 -6
  446. package/manager/processes/restartServer.test.mjs +162 -0
  447. package/manager/processes/shutdownServer.mjs +11 -0
  448. package/manager/processes/shutdownServer.test.mjs +45 -0
  449. package/manager/processes/startFirstServer.mjs +46 -0
  450. package/manager/processes/startFirstServer.test.mjs +326 -0
  451. package/manager/processes/startMailSink.mjs +135 -0
  452. package/manager/processes/startMailSink.test.mjs +146 -0
  453. package/manager/processes/startProxy.mjs +129 -12
  454. package/manager/processes/startProxy.test.mjs +366 -0
  455. package/manager/processes/startServer.mjs +47 -6
  456. package/manager/processes/startServer.test.mjs +155 -0
  457. package/manager/processes/startWatchers.mjs +6 -4
  458. package/manager/processes/syncServer.mjs +69 -0
  459. package/manager/processes/syncServer.test.mjs +141 -0
  460. package/manager/processes/waitForDevServer.mjs +48 -0
  461. package/manager/processes/waitForDevServer.test.mjs +112 -0
  462. package/manager/run.mjs +128 -43
  463. package/manager/utils/BatchChanges.mjs +21 -5
  464. package/manager/utils/BatchChanges.test.mjs +77 -0
  465. package/manager/utils/acquireDevInstance.mjs +99 -0
  466. package/manager/utils/acquireDevInstance.test.mjs +160 -0
  467. package/manager/utils/createBuildActivity.mjs +67 -0
  468. package/manager/utils/createBuildActivity.test.mjs +58 -0
  469. package/manager/utils/createChangeSignal.mjs +39 -0
  470. package/manager/utils/createChangeSignal.test.mjs +62 -0
  471. package/manager/utils/createCustomPluginMessagesMap.mjs +23 -33
  472. package/manager/utils/createCustomPluginTypesMap.mjs +24 -30
  473. package/manager/utils/createCustomPluginTypesMap.test.mjs +94 -0
  474. package/manager/utils/createDotPathIgnore.mjs +44 -0
  475. package/manager/utils/createDotPathIgnore.test.mjs +65 -0
  476. package/manager/utils/createIdleGc.mjs +51 -0
  477. package/manager/utils/createIdleGc.test.mjs +77 -0
  478. package/manager/utils/createRequestActivity.mjs +100 -0
  479. package/manager/utils/createRequestActivity.test.mjs +121 -0
  480. package/manager/utils/createServerArtifactTracker.mjs +83 -0
  481. package/manager/utils/createServerArtifactTracker.test.mjs +78 -0
  482. package/manager/utils/createServerEnv.mjs +39 -0
  483. package/manager/utils/filesHaveSameBytes.mjs +41 -0
  484. package/manager/utils/findBuildFilesOutsideWatch.mjs +59 -0
  485. package/manager/utils/findBuildFilesOutsideWatch.test.mjs +111 -0
  486. package/manager/utils/importFresh.mjs +46 -0
  487. package/manager/utils/importFresh.test.mjs +72 -0
  488. package/manager/utils/importFreshWorker.mjs +20 -0
  489. package/manager/utils/killTaggedBrowser.mjs +35 -0
  490. package/manager/utils/pagePathChanged.mjs +56 -0
  491. package/manager/utils/pruneRecordings.mjs +85 -0
  492. package/manager/utils/pruneRecordings.test.mjs +117 -0
  493. package/manager/utils/publishBuildDirectory.mjs +121 -0
  494. package/manager/utils/publishBuildDirectory.order.test.mjs +74 -0
  495. package/manager/utils/publishBuildDirectory.test.mjs +262 -0
  496. package/manager/utils/readBasePath.mjs +27 -0
  497. package/manager/utils/readBasePath.test.mjs +43 -0
  498. package/manager/utils/readBuildStatusWait.mjs +67 -0
  499. package/manager/utils/readManagerVersion.mjs +30 -0
  500. package/manager/utils/readManagerVersion.test.mjs +35 -0
  501. package/manager/utils/readPluginDefinitions.mjs +36 -0
  502. package/manager/utils/resolveDevAuthUrl.mjs +58 -0
  503. package/manager/utils/resolveDevAuthUrl.test.mjs +107 -0
  504. package/manager/utils/resolvePorts.mjs +49 -0
  505. package/manager/utils/setupWatcher.mjs +19 -11
  506. package/manager/utils/startRecordingPruner.mjs +45 -0
  507. package/manager/utils/startRecordingPruner.test.mjs +66 -0
  508. package/manager/utils/waitForChildExit.mjs +53 -0
  509. package/manager/utils/waitForChildExit.test.mjs +70 -0
  510. package/manager/utils/waitForServer.mjs +44 -0
  511. package/manager/utils/writeBuildStatus.mjs +1 -1
  512. package/manager/watchers/createPluginSourceIgnore.mjs +77 -0
  513. package/manager/watchers/envWatcher.mjs +9 -2
  514. package/manager/watchers/envWatcher.test.mjs +87 -0
  515. package/manager/watchers/listServerSidePackages.mjs +47 -0
  516. package/manager/watchers/listServerSidePackages.test.mjs +38 -0
  517. package/manager/watchers/lowdefyBuildWatcher.mjs +106 -22
  518. package/manager/watchers/lowdefyBuildWatcher.test.mjs +309 -0
  519. package/manager/watchers/pluginSourceWatcher.mjs +79 -0
  520. package/manager/watchers/pluginSourceWatcher.test.mjs +222 -0
  521. package/manager/watchers/restartRequestWatcher.mjs +71 -0
  522. package/manager/watchers/restartRequestWatcher.test.mjs +111 -0
  523. package/manager/watchers/selectLocalPluginPackages.mjs +48 -0
  524. package/manager/watchers/selectLocalPluginPackages.test.mjs +73 -0
  525. package/manager/watchers/serverPackageWatcher.mjs +38 -0
  526. package/package.json +61 -49
  527. package/package.original.json +61 -49
  528. package/src/app.js +131 -20
  529. package/src/html/renderDevPage.js +63 -11
  530. package/src/html/renderDevPage.test.mjs +138 -0
  531. package/src/lib/getRequestPath.js +29 -0
  532. package/src/middleware/createSameOriginGuard.js +37 -0
  533. package/src/middleware/createSameOriginGuard.test.mjs +102 -0
  534. package/src/middleware/errorHandler.js +51 -1
  535. package/src/middleware/errorHandler.test.mjs +196 -14
  536. package/src/middleware/isRebindingSafeHost.js +44 -0
  537. package/src/middleware/isRebindingSafeHost.test.mjs +32 -0
  538. package/src/middleware/localDevToolsOnly.js +38 -0
  539. package/src/middleware/localDevToolsOnly.test.mjs +61 -0
  540. package/src/middleware/noStore.js +27 -0
  541. package/src/middleware/noStore.test.mjs +48 -0
  542. package/src/middleware/requestTimeout.js +38 -0
  543. package/src/middleware/staleFlag.js +76 -0
  544. package/src/middleware/staleFlag.test.mjs +127 -0
  545. package/src/middleware/stampCaller.js +38 -0
  546. package/src/middleware/stampCaller.test.mjs +64 -0
  547. package/src/routes/agent.js +15 -3
  548. package/src/routes/auth.js +29 -20
  549. package/src/routes/auth.test.mjs +30 -56
  550. package/src/routes/clientError.js +13 -13
  551. package/src/routes/cron.js +3 -1
  552. package/src/routes/cronForward.js +4 -2
  553. package/src/routes/detached.js +22 -9
  554. package/src/routes/devInspect.js +17 -8
  555. package/src/routes/devInspect.test.mjs +44 -7
  556. package/src/routes/devRecording.js +113 -0
  557. package/src/routes/devRecording.test.mjs +253 -0
  558. package/src/routes/docs/buildStatus.js +10 -2
  559. package/src/routes/docs/buildStatus.test.mjs +46 -0
  560. package/src/routes/docs/content.js +5 -3
  561. package/src/routes/docs/evalOperator.js +15 -1
  562. package/src/routes/docs/events.js +50 -0
  563. package/src/routes/docs/events.test.mjs +117 -0
  564. package/src/routes/docs/find.js +9 -1
  565. package/src/routes/docs/icons.js +30 -0
  566. package/src/routes/docs/inspectState.js +16 -1
  567. package/src/routes/docs/journey.js +194 -0
  568. package/src/routes/docs/journey.test.mjs +401 -0
  569. package/src/routes/docs/journeyRefusals.test.mjs +71 -0
  570. package/src/routes/docs/loadState.js +4 -2
  571. package/src/routes/docs/mcp.js +34 -3
  572. package/src/routes/docs/mcp.test.mjs +130 -0
  573. package/src/routes/docs/mutants.js +78 -0
  574. package/src/routes/docs/mutants.test.mjs +180 -0
  575. package/src/routes/docs/parsePathParamsParam.js +41 -0
  576. package/src/routes/docs/parsePathParamsParam.test.mjs +42 -0
  577. package/src/routes/docs/parseUserParam.js +27 -21
  578. package/src/routes/docs/parseUserParam.test.mjs +17 -5
  579. package/src/routes/docs/restart.js +35 -0
  580. package/src/routes/docs/restart.test.mjs +64 -0
  581. package/src/routes/docs/runEndpoint.js +54 -0
  582. package/src/routes/docs/runEndpoint.test.mjs +165 -0
  583. package/src/routes/docs/runRequest.js +25 -8
  584. package/src/routes/docs/runRequest.test.mjs +92 -0
  585. package/src/routes/docs/screenshot.js +50 -2
  586. package/src/routes/docs/screenshot.test.mjs +117 -0
  587. package/src/routes/docs/search.js +15 -2
  588. package/src/routes/docs/snapshotState.js +17 -3
  589. package/src/routes/endpoints.js +27 -26
  590. package/src/routes/endpointsTextAnswer.test.mjs +77 -0
  591. package/src/routes/feedback.js +10 -13
  592. package/src/routes/jitPage.js +99 -35
  593. package/src/routes/jitPage.test.mjs +248 -0
  594. package/src/routes/mcp.js +82 -5
  595. package/src/routes/mcp.test.mjs +215 -0
  596. package/src/routes/mcpMemberToken.test.mjs +125 -0
  597. package/src/routes/mcpProtectedResourceMetadata.js +34 -0
  598. package/src/routes/mcpProtectedResourceMetadata.test.mjs +65 -0
  599. package/src/routes/mountDevPageRoutes.js +25 -0
  600. package/src/routes/mountDevPageRoutes.test.mjs +30 -0
  601. package/src/routes/mountOauthDiscovery.js +44 -0
  602. package/src/routes/mountOauthDiscovery.test.mjs +79 -0
  603. package/src/routes/normalizeMcpProtocolVersionHeader.js +46 -0
  604. package/src/routes/normalizeMcpProtocolVersionHeader.test.mjs +62 -0
  605. package/src/routes/reload.js +16 -0
  606. package/src/routes/request.js +6 -4
  607. package/src/routes/request.test.mjs +9 -5
  608. package/src/routes/requestBody.test.mjs +159 -0
  609. package/src/routes/usage.js +6 -2
  610. package/src/routes/user.js +27 -0
  611. package/src/routes/websocket.js +29 -8
  612. package/src/routes/websocket.test.mjs +65 -0
  613. package/src/routes/wellKnownFallback.js +30 -0
  614. package/src/routes/wellKnownFallback.test.mjs +32 -0
  615. package/src/websocket/devWebSocket.js +41 -26
  616. package/src/websocket/devWebSocket.test.mjs +157 -0
  617. package/vite.config.js +53 -23
  618. package/lib/server/auth/getDevSession.js +0 -70
  619. package/lib/server/auth/getDevSession.test.mjs +0 -138
  620. package/lib/server/auth/session.js +0 -39
  621. package/manager/processes/warnAuthUrlPortMismatch.mjs +0 -47
  622. package/manager/processes/warnAuthUrlPortMismatch.test.mjs +0 -93
  623. package/manager/watchers/moduleBuildWatcher.mjs +0 -79
  624. package/manager/watchers/serverArtifactWatcher.mjs +0 -120
  625. package/src/routes/apiPage.js +0 -37
@@ -17,6 +17,11 @@
17
17
  import http from 'node:http';
18
18
  import net from 'node:net';
19
19
 
20
+ import { devPassiveHeader } from '@lowdefy/node-utils';
21
+
22
+ import buildWaitHeader from '../../lib/docs/buildWaitHeader.js';
23
+ import readBuildStatusWait from '../utils/readBuildStatusWait.mjs';
24
+
20
25
  /*
21
26
  The manager owns the public port; the Vite child listens on an internal
22
27
  loopback port and every restart replaces only the child. Before this proxy the
@@ -32,6 +37,19 @@ While the child is down, requests and upgrades wait for it to come back
32
37
  covers a Vite respawn with headroom; a child that stays down longer than that
33
38
  answers 503 so callers are not held forever. In-flight streams cannot survive
34
39
  a child exit — those sockets close and the client reconnects into the hold.
40
+
41
+ A child is probed until it answers once; after that, requests go straight to
42
+ it for as long as that child process lives. Probing every request opened (and
43
+ closed) one extra TCP connection per request, and a Vite page load is hundreds
44
+ of module requests: each closed probe sits in TIME_WAIT, and a few dozen page
45
+ loads within half a minute ran the machine out of ephemeral ports, failing
46
+ unrelated connections. A proxied request that cannot reach the child, a child
47
+ that exits, or a new child after a restart sends requests back through the
48
+ probe and its hold. A restart first waits for the stopped child to exit, since
49
+ until then it still answers the probe. A GET or HEAD that never reached the
50
+ child (a refused connect, or a keep-alive socket the dying child had closed)
51
+ is replayed through the hold rather than answered 502: a child can close its
52
+ sockets before the manager sees it exit.
35
53
  */
36
54
 
37
55
  const RETRY_MS = 250;
@@ -51,8 +69,7 @@ function probeChild(port) {
51
69
  });
52
70
  }
53
71
 
54
- async function waitForChild({ port }) {
55
- const deadline = Date.now() + HOLD_MS;
72
+ async function waitForChild({ port, deadline }) {
56
73
  for (;;) {
57
74
  if (await probeChild(port)) return true;
58
75
  if (Date.now() > deadline) return false;
@@ -60,14 +77,57 @@ async function waitForChild({ port }) {
60
77
  }
61
78
  }
62
79
 
63
- function forwardRequest(context, req, res) {
80
+ function isConfirmedChild({ context, proxyState }) {
81
+ const child = context.devServer;
82
+ return (
83
+ Boolean(child) &&
84
+ child === proxyState.confirmedChild &&
85
+ child.exitCode === null &&
86
+ child.signalCode === null
87
+ );
88
+ }
89
+
90
+ function waitUntil({ promise, deadline }) {
91
+ return Promise.race([
92
+ promise,
93
+ new Promise((resolve) => setTimeout(resolve, Math.max(0, deadline - Date.now()))),
94
+ ]);
95
+ }
96
+
97
+ async function waitForConfirmedChild({ context, proxyState, deadline }) {
98
+ if (isConfirmedChild({ context, proxyState })) {
99
+ return true;
100
+ }
101
+ await waitUntil({ promise: context.devServerExited, deadline });
102
+ const child = context.devServer;
103
+ const up = await waitForChild({ port: context.internalPort, deadline });
104
+ if (up) {
105
+ proxyState.confirmedChild = child;
106
+ }
107
+ return up;
108
+ }
109
+
110
+ const REPLAYABLE_METHODS = ['GET', 'HEAD'];
111
+
112
+ // Nothing reached the child: the connect was refused, or the keep-alive socket
113
+ // the agent reused had already been closed by a child that is going away.
114
+ function isUnsentError({ error, proxyReq }) {
115
+ return (
116
+ error.code === 'ECONNREFUSED' || (proxyReq.reusedSocket === true && error.code === 'ECONNRESET')
117
+ );
118
+ }
119
+
120
+ function forwardRequest({ body, context, proxyState }, req, res, deadline = Date.now() + HOLD_MS) {
64
121
  // Wait for a live child BEFORE piping the request body — the body stream can
65
122
  // only be consumed once, so retrying after a failed proxy request would need
66
- // full-body buffering. A probe-then-forward race (child dies between the
67
- // probe and the connect) surfaces as one 502, which the client's next
68
- // attempt resolves through the hold.
69
- waitForChild({ port: context.internalPort }).then((up) => {
70
- if (req.destroyed) return;
123
+ // full-body buffering. Only a bodiless GET or HEAD that never reached the
124
+ // child is replayed; any other request that loses the child mid-forward
125
+ // surfaces as one 502, which the client's next attempt resolves through the
126
+ // hold.
127
+ waitForConfirmedChild({ context, proxyState, deadline }).then((up) => {
128
+ // The response, not the request: an incoming request reads as destroyed
129
+ // once its body has been read, which a replayed GET's has.
130
+ if (res.destroyed) return;
71
131
  if (!up) {
72
132
  res.writeHead(503, { 'content-type': 'application/json' });
73
133
  res.end(JSON.stringify({ message: 'Lowdefy dev server is restarting.' }));
@@ -87,7 +147,16 @@ function forwardRequest(context, req, res) {
87
147
  res.flushHeaders?.();
88
148
  proxyRes.pipe(res);
89
149
  });
90
- proxyReq.on('error', () => {
150
+ proxyReq.on('error', (error) => {
151
+ proxyState.confirmedChild = null;
152
+ if (
153
+ !res.headersSent &&
154
+ REPLAYABLE_METHODS.includes(req.method) &&
155
+ isUnsentError({ error, proxyReq })
156
+ ) {
157
+ forwardRequest({ body, context, proxyState }, req, res, deadline);
158
+ return;
159
+ }
91
160
  if (res.headersSent) {
92
161
  res.destroy();
93
162
  return;
@@ -95,7 +164,16 @@ function forwardRequest(context, req, res) {
95
164
  res.writeHead(502, { 'content-type': 'application/json' });
96
165
  res.end(JSON.stringify({ message: 'Lowdefy dev server connection dropped.' }));
97
166
  });
98
- req.pipe(proxyReq);
167
+ // A body read to recognise a build-status wait is sent as read. A
168
+ // replayed GET's request stream has already ended, so there is nothing
169
+ // left to pipe.
170
+ if (body !== undefined) {
171
+ proxyReq.end(body);
172
+ } else if (req.readableEnded) {
173
+ proxyReq.end();
174
+ } else {
175
+ req.pipe(proxyReq);
176
+ }
99
177
  req.on('error', () => proxyReq.destroy());
100
178
  // A client that goes away mid-response (a closed browser tab holding the
101
179
  // reload SSE stream, an agent dropping its MCP stream) must reach the
@@ -107,8 +185,43 @@ function forwardRequest(context, req, res) {
107
185
  });
108
186
  }
109
187
 
188
+ // Activity is use of the server. Not counted: a passive look (the hub's tab
189
+ // poll, an agent's status read), and long-lived streams - an event stream a
190
+ // client opens with a GET (the agent shim's MCP push stream, a tab's reload
191
+ // stream) or a websocket - which live as long as the server and reconnect on
192
+ // their own, so counting them would keep every server they touch alive. An
193
+ // MCP tool call is a POST that may be answered as an event stream; it counts.
194
+ function isActivity(req) {
195
+ if (req.headers[devPassiveHeader] === '1') {
196
+ return false;
197
+ }
198
+ const accept = req.headers.accept ?? '';
199
+ return !(req.method === 'GET' && accept.includes('text/event-stream'));
200
+ }
201
+
202
+ // A build-status wait is held here until the manager has processed the
203
+ // latest edits, restarts included, and only then forwarded: a restart ends
204
+ // the dev server process, and any wait running in it. The header tells the
205
+ // dev server what this wait saw, so it does not wait again.
206
+ async function handleRequest({ context, proxyState }, req, res) {
207
+ if (isActivity(req)) {
208
+ context.requestActivity.trackResponse(res);
209
+ }
210
+ delete req.headers[buildWaitHeader];
211
+ const { wait, body } = await readBuildStatusWait({ basePath: context.basePath, req });
212
+ if (wait) {
213
+ const waited = await context.buildActivity.waitForIdle();
214
+ req.headers[buildWaitHeader] = new URLSearchParams({
215
+ settled: String(waited.settled),
216
+ sawBuild: String(waited.sawBuild),
217
+ waitedMs: String(waited.waitedMs),
218
+ }).toString();
219
+ }
220
+ forwardRequest({ body, context, proxyState }, req, res);
221
+ }
222
+
110
223
  function forwardUpgrade(context, req, socket, head) {
111
- waitForChild({ port: context.internalPort }).then((up) => {
224
+ waitForChild({ port: context.internalPort, deadline: Date.now() + HOLD_MS }).then((up) => {
112
225
  if (socket.destroyed) return;
113
226
  if (!up) {
114
227
  socket.end('HTTP/1.1 503 Service Unavailable\r\nconnection: close\r\n\r\n');
@@ -136,7 +249,11 @@ function forwardUpgrade(context, req, socket, head) {
136
249
 
137
250
  function startProxy(context) {
138
251
  if (context.proxyServer) return Promise.resolve();
139
- const proxy = http.createServer((req, res) => forwardRequest(context, req, res));
252
+ const proxyState = { confirmedChild: null };
253
+ const proxy = http.createServer((req, res) => {
254
+ // A client that drops an MCP request while its body is read.
255
+ handleRequest({ context, proxyState }, req, res).catch(() => res.destroy());
256
+ });
140
257
  proxy.on('upgrade', (req, socket, head) => forwardUpgrade(context, req, socket, head));
141
258
  // Long-lived streams (SSE, MCP) must not be reaped by the default 5-minute
142
259
  // request timeout; keep the proxy transparent.
@@ -20,6 +20,8 @@ import net from 'node:net';
20
20
  import { jest } from '@jest/globals';
21
21
 
22
22
  const { default: startProxy } = await import('./startProxy.mjs');
23
+ const { default: createBuildActivity } = await import('../utils/createBuildActivity.mjs');
24
+ const { default: createRequestActivity } = await import('../utils/createRequestActivity.mjs');
23
25
 
24
26
  function listen(server) {
25
27
  return new Promise((resolve) => {
@@ -46,6 +48,7 @@ function getFreePort() {
46
48
 
47
49
  let child;
48
50
  let context;
51
+ let activityRecords;
49
52
 
50
53
  afterEach(async () => {
51
54
  if (context?.proxyServer) await close(context.proxyServer);
@@ -57,10 +60,15 @@ afterEach(async () => {
57
60
  async function startChildAndProxy(handler) {
58
61
  child = http.createServer(handler);
59
62
  const internalPort = await listen(child);
63
+ activityRecords = [];
60
64
  context = {
61
65
  internalPort,
62
66
  options: { port: await getFreePort() },
63
67
  logger: { debug: jest.fn() },
68
+ requestActivity: createRequestActivity({
69
+ onChange: (fields) => activityRecords.push(fields),
70
+ throttleMs: 0,
71
+ }),
64
72
  };
65
73
  await startProxy(context);
66
74
  return context.options.port;
@@ -76,6 +84,101 @@ test('startProxy forwards a request to the child and relays its response', async
76
84
  expect(await response.json()).toEqual({ path: '/api/ping?x=1' });
77
85
  });
78
86
 
87
+ test('startProxy probes a running child once instead of opening a connection per request', async () => {
88
+ let childConnections = 0;
89
+ const port = await startChildAndProxy((req, res) => {
90
+ res.writeHead(200, { 'content-type': 'text/plain' });
91
+ res.end('ok');
92
+ });
93
+ child.on('connection', () => {
94
+ childConnections += 1;
95
+ });
96
+ context.devServer = { exitCode: null, signalCode: null };
97
+
98
+ for (let i = 0; i < 20; i += 1) {
99
+ const response = await fetch(`http://localhost:${port}/module-${i}.js`);
100
+ expect(await response.text()).toBe('ok');
101
+ }
102
+
103
+ // One probe, then the keep-alive agent's socket - not a probe per request.
104
+ expect(childConnections).toBeLessThanOrEqual(2);
105
+ });
106
+
107
+ test('startProxy probes a new child after a restart before forwarding to it', async () => {
108
+ let childConnections = 0;
109
+ const port = await startChildAndProxy((req, res) => {
110
+ res.writeHead(200, { 'content-type': 'text/plain' });
111
+ res.end('ok');
112
+ });
113
+ child.on('connection', () => {
114
+ childConnections += 1;
115
+ });
116
+ context.devServer = { exitCode: null, signalCode: null };
117
+ await fetch(`http://localhost:${port}/a.js`).then((response) => response.text());
118
+ const afterFirstChild = childConnections;
119
+
120
+ context.devServer = { exitCode: null, signalCode: null };
121
+ await fetch(`http://localhost:${port}/b.js`).then((response) => response.text());
122
+
123
+ expect(childConnections).toBe(afterFirstChild + 1);
124
+ });
125
+
126
+ test('startProxy sends requests back through the hold once a confirmed child goes away', async () => {
127
+ function handler(req, res) {
128
+ res.end('ok');
129
+ }
130
+ const port = await startChildAndProxy(handler);
131
+ context.devServer = { exitCode: null, signalCode: null };
132
+ expect(await (await fetch(`http://localhost:${port}/a.js`)).text()).toBe('ok');
133
+
134
+ async function stopChildAndRestartSoon() {
135
+ await close(child);
136
+ setTimeout(() => {
137
+ child = http.createServer(handler);
138
+ child.listen(context.internalPort, '127.0.0.1');
139
+ }, 300);
140
+ }
141
+
142
+ // Gone before the manager has seen it exit: a GET never reached the child,
143
+ // so it is replayed through the hold and answered by the next child.
144
+ await stopChildAndRestartSoon();
145
+ expect(await (await fetch(`http://localhost:${port}/b.js`)).text()).toBe('ok');
146
+
147
+ // A POST's body was consumed by the failed forward, so it fails once, and
148
+ // the next request waits for the child instead of failing too.
149
+ await stopChildAndRestartSoon();
150
+ expect(
151
+ (await fetch(`http://localhost:${port}/save`, { method: 'POST', body: '{}' })).status
152
+ ).toBe(502);
153
+ expect(await (await fetch(`http://localhost:${port}/c.js`)).text()).toBe('ok');
154
+
155
+ // An exit the manager has seen sends the very next request through the hold.
156
+ await stopChildAndRestartSoon();
157
+ context.devServer.exitCode = 1;
158
+ expect(await (await fetch(`http://localhost:${port}/d.js`)).text()).toBe('ok');
159
+ });
160
+
161
+ test('startProxy waits for a stopped child to exit before probing its replacement', async () => {
162
+ const port = await startChildAndProxy((req, res) => res.end('old'));
163
+ context.devServer = { exitCode: null, signalCode: null };
164
+ expect(await (await fetch(`http://localhost:${port}/a.js`)).text()).toBe('old');
165
+
166
+ // A restart: the old child is signalled but answers until it exits.
167
+ let markExited;
168
+ context.devServerExited = new Promise((resolve) => {
169
+ markExited = resolve;
170
+ });
171
+ context.devServer = { exitCode: null, signalCode: null };
172
+ const pending = fetch(`http://localhost:${port}/b.js`).then((response) => response.text());
173
+ await new Promise((resolve) => setTimeout(resolve, 100));
174
+ await close(child);
175
+ child = http.createServer((req, res) => res.end('new'));
176
+ await new Promise((resolve) => child.listen(context.internalPort, '127.0.0.1', resolve));
177
+ markExited();
178
+
179
+ expect(await pending).toBe('new');
180
+ });
181
+
79
182
  test('startProxy aborts the child request when the client drops a streaming response', async () => {
80
183
  let upstreamClosed;
81
184
  const upstreamClosedPromise = new Promise((resolve) => {
@@ -100,3 +203,266 @@ test('startProxy aborts the child request when the client drops a streaming resp
100
203
  ]);
101
204
  expect(outcome).toBe('closed');
102
205
  });
206
+
207
+ function echoRequest(label) {
208
+ return async (req, res) => {
209
+ const chunks = [];
210
+ for await (const chunk of req) chunks.push(chunk);
211
+ res.writeHead(200, { 'content-type': 'application/json' });
212
+ res.end(
213
+ JSON.stringify({
214
+ label,
215
+ body: Buffer.concat(chunks).toString('utf8'),
216
+ buildWait: req.headers['x-lowdefy-build-wait'] ?? null,
217
+ })
218
+ );
219
+ };
220
+ }
221
+
222
+ function mcpCall(name, args) {
223
+ return JSON.stringify({
224
+ jsonrpc: '2.0',
225
+ id: 1,
226
+ method: 'tools/call',
227
+ params: { name, arguments: args },
228
+ });
229
+ }
230
+
231
+ test.each([
232
+ ['GET build-status?wait=true', () => ({ path: '/lowdefy-docs/build-status?wait=true' })],
233
+ [
234
+ 'the lowdefy_build_status MCP tool with wait: true',
235
+ () => ({
236
+ path: '/lowdefy-docs/mcp',
237
+ init: { method: 'POST', body: mcpCall('lowdefy_build_status', { wait: true }) },
238
+ }),
239
+ ],
240
+ ])(
241
+ 'startProxy holds %s through a restart and forwards it to the new server',
242
+ async (_, makeRequest) => {
243
+ const port = await startChildAndProxy(echoRequest('old'));
244
+ context.basePath = '';
245
+ context.buildActivity = createBuildActivity({ onChange: () => {} });
246
+ context.devServer = { exitCode: null, signalCode: null };
247
+ context.buildActivity.setBusy(true);
248
+ // waitedMs is timed from when the proxy starts waiting, a moment after the request is sent.
249
+ // Holding for 200ms from that moment, not from the fetch call, keeps the waitedMs assertion
250
+ // below from racing the request's arrival.
251
+ const { waitForIdle } = context.buildActivity;
252
+ const waitStarted = new Promise((resolve) => {
253
+ context.buildActivity.waitForIdle = (options) => {
254
+ const waiting = waitForIdle(options);
255
+ resolve();
256
+ return waiting;
257
+ };
258
+ });
259
+
260
+ const { path: requestPath, init } = makeRequest();
261
+ const pending = fetch(`http://localhost:${port}${requestPath}`, {
262
+ ...init,
263
+ headers: { 'x-lowdefy-build-wait': 'settled=true&sawBuild=false&waitedMs=0' },
264
+ }).then((response) => response.json());
265
+ await waitStarted;
266
+ await new Promise((resolve) => setTimeout(resolve, 200));
267
+
268
+ // The restart the build needed: the old server stops, a new one answers.
269
+ context.devServerExited = Promise.resolve();
270
+ context.devServer = { exitCode: null, signalCode: null };
271
+ await close(child);
272
+ child = http.createServer(echoRequest('new'));
273
+ await new Promise((resolve) => child.listen(context.internalPort, '127.0.0.1', resolve));
274
+ context.buildActivity.setBusy(false);
275
+
276
+ const result = await pending;
277
+ expect(result.label).toBe('new');
278
+ expect(result.body).toBe(init?.body ?? '');
279
+ const wait = new URLSearchParams(result.buildWait);
280
+ expect(wait.get('settled')).toBe('true');
281
+ expect(wait.get('sawBuild')).toBe('true');
282
+ expect(Number(wait.get('waitedMs'))).toBeGreaterThanOrEqual(200);
283
+ }
284
+ );
285
+
286
+ test('startProxy forwards other MCP calls and build-status reads at once, with their body', async () => {
287
+ const port = await startChildAndProxy(echoRequest('child'));
288
+ context.basePath = '';
289
+ context.buildActivity = createBuildActivity({ onChange: () => {} });
290
+ context.buildActivity.setBusy(true);
291
+ const body = mcpCall('lowdefy_build_status', {});
292
+
293
+ const [call, read] = await Promise.all([
294
+ fetch(`http://localhost:${port}/lowdefy-docs/mcp`, {
295
+ method: 'POST',
296
+ body,
297
+ headers: { 'x-lowdefy-build-wait': 'settled=true' },
298
+ }).then((response) => response.json()),
299
+ fetch(`http://localhost:${port}/lowdefy-docs/build-status`).then((response) => response.json()),
300
+ ]);
301
+
302
+ expect(call).toEqual({ label: 'child', body, buildWait: null });
303
+ expect(read).toEqual({ label: 'child', body: '', buildWait: null });
304
+ });
305
+
306
+ const paddedWaitCall = mcpCall('lowdefy_build_status', { wait: true, pad: 'x'.repeat(70 * 1024) });
307
+ const waitCall = mcpCall('lowdefy_build_status', { wait: true });
308
+
309
+ test.each([
310
+ ['a body larger than 64 KiB', { body: paddedWaitCall }, paddedWaitCall],
311
+ [
312
+ 'a streamed body without a length',
313
+ { body: new Blob([waitCall]).stream(), duplex: 'half' },
314
+ waitCall,
315
+ ],
316
+ ])('startProxy forwards an MCP POST with %s unread, without holding it', async (_, init, sent) => {
317
+ const port = await startChildAndProxy(echoRequest('child'));
318
+ context.basePath = '';
319
+ context.buildActivity = createBuildActivity({ onChange: () => {} });
320
+ context.buildActivity.setBusy(true);
321
+
322
+ const call = await fetch(`http://localhost:${port}/lowdefy-docs/mcp`, {
323
+ method: 'POST',
324
+ ...init,
325
+ }).then((response) => response.json());
326
+
327
+ expect(call).toEqual({ label: 'child', body: sent, buildWait: null });
328
+ });
329
+
330
+ function activeRequests() {
331
+ return activityRecords.at(-1).activeRequests;
332
+ }
333
+
334
+ async function waitFor(check) {
335
+ const deadline = Date.now() + 2000;
336
+ while (!check()) {
337
+ if (Date.now() > deadline) throw new Error('Condition not met within 2 seconds.');
338
+ await new Promise((resolve) => setTimeout(resolve, 10));
339
+ }
340
+ }
341
+
342
+ // A child that holds every response open until release() is called.
343
+ function heldHandler({ contentType = 'text/plain' } = {}) {
344
+ const held = [];
345
+ function handler(req, res) {
346
+ res.writeHead(200, { 'content-type': contentType });
347
+ res.flushHeaders();
348
+ if (contentType === 'text/event-stream') res.write('event: message\ndata: {}\n\n');
349
+ held.push(res);
350
+ }
351
+ function release() {
352
+ held.splice(0).forEach((res) => res.end('done'));
353
+ }
354
+ return { handler, held, release };
355
+ }
356
+
357
+ test('startProxy counts a request as activity for as long as it is in flight', async () => {
358
+ const child$ = heldHandler();
359
+ const port = await startChildAndProxy(child$.handler);
360
+ context.basePath = '';
361
+ expect(activeRequests()).toBe(0);
362
+
363
+ const pending = fetch(`http://localhost:${port}/api/request/page/get`).then((response) =>
364
+ response.text()
365
+ );
366
+ await waitFor(() => child$.held.length === 1);
367
+ expect(activeRequests()).toBe(1);
368
+
369
+ child$.release();
370
+ expect(await pending).toBe('done');
371
+ await waitFor(() => activeRequests() === 0);
372
+ });
373
+
374
+ test('startProxy counts an MCP POST answered as an event stream until it ends', async () => {
375
+ const child$ = heldHandler({ contentType: 'text/event-stream' });
376
+ const port = await startChildAndProxy(child$.handler);
377
+ context.basePath = '';
378
+
379
+ const pending = fetch(`http://localhost:${port}/lowdefy-docs/mcp`, {
380
+ method: 'POST',
381
+ headers: { accept: 'application/json, text/event-stream' },
382
+ body: mcpCall('lowdefy_screenshot', {}),
383
+ }).then((response) => response.text());
384
+ await waitFor(() => child$.held.length === 1);
385
+ expect(activeRequests()).toBe(1);
386
+
387
+ child$.release();
388
+ await pending;
389
+ await waitFor(() => activeRequests() === 0);
390
+ });
391
+
392
+ test.each([
393
+ ['a GET that opens an event stream', { accept: 'text/event-stream' }],
394
+ ['a passive request', { 'x-lowdefy-passive': '1' }],
395
+ ])('startProxy does not count %s as activity', async (_, headers) => {
396
+ const child$ = heldHandler({ contentType: 'text/event-stream' });
397
+ const port = await startChildAndProxy(child$.handler);
398
+ context.basePath = '';
399
+ const before = activityRecords.length;
400
+
401
+ const clientRequest = http.get(`http://localhost:${port}/lowdefy-docs/mcp`, { headers });
402
+ await new Promise((resolve) => clientRequest.on('response', resolve));
403
+ await waitFor(() => child$.held.length === 1);
404
+ clientRequest.destroy();
405
+ child$.release();
406
+ await new Promise((resolve) => setTimeout(resolve, 50));
407
+
408
+ expect(activityRecords.length).toBe(before);
409
+ });
410
+
411
+ test('startProxy ends the activity of a request whose client goes away', async () => {
412
+ const child$ = heldHandler();
413
+ const port = await startChildAndProxy(child$.handler);
414
+ context.basePath = '';
415
+
416
+ const clientRequest = http.get(`http://localhost:${port}/slow`);
417
+ clientRequest.on('error', () => {});
418
+ await waitFor(() => child$.held.length === 1);
419
+ expect(activeRequests()).toBe(1);
420
+
421
+ clientRequest.destroy();
422
+ await waitFor(() => activeRequests() === 0);
423
+ child$.release();
424
+ });
425
+
426
+ test('startProxy does not count a websocket upgrade as activity', async () => {
427
+ const port = await startChildAndProxy((req, res) => res.end('ok'));
428
+ const childSockets = [];
429
+ child.on('upgrade', (req, socket) => {
430
+ childSockets.push(socket);
431
+ socket.write(
432
+ 'HTTP/1.1 101 Switching Protocols\r\nupgrade: websocket\r\nconnection: upgrade\r\n\r\n'
433
+ );
434
+ });
435
+ const before = activityRecords.length;
436
+
437
+ const clientRequest = http.get(`http://localhost:${port}/hmr`, {
438
+ headers: { connection: 'upgrade', upgrade: 'websocket' },
439
+ });
440
+ const socket = await new Promise((resolve) =>
441
+ clientRequest.on('upgrade', (res, upgraded) => resolve(upgraded))
442
+ );
443
+
444
+ expect(activityRecords.length).toBe(before);
445
+ socket.destroy();
446
+ childSockets.forEach((childSocket) => childSocket.destroy());
447
+ });
448
+
449
+ test('startProxy keeps a request held through a child restart counted', async () => {
450
+ const port = await startChildAndProxy((req, res) => res.end('old'));
451
+ context.basePath = '';
452
+ context.devServer = { exitCode: null, signalCode: null };
453
+ expect(await (await fetch(`http://localhost:${port}/a.js`)).text()).toBe('old');
454
+ await waitFor(() => activeRequests() === 0);
455
+
456
+ await close(child);
457
+ context.devServer.exitCode = 1;
458
+ const pending = fetch(`http://localhost:${port}/b.js`).then((response) => response.text());
459
+ await new Promise((resolve) => setTimeout(resolve, 300));
460
+ expect(activeRequests()).toBe(1);
461
+
462
+ context.devServer = { exitCode: null, signalCode: null };
463
+ child = http.createServer((req, res) => res.end('new'));
464
+ await new Promise((resolve) => child.listen(context.internalPort, '127.0.0.1', resolve));
465
+
466
+ expect(await pending).toBe('new');
467
+ await waitFor(() => activeRequests() === 0);
468
+ });
@@ -15,8 +15,12 @@
15
15
  */
16
16
 
17
17
  import { spawn } from 'child_process';
18
+ import crypto from 'crypto';
18
19
 
19
- import warnAuthUrlPortMismatch from './warnAuthUrlPortMismatch.mjs';
20
+ import createServerEnv from '../utils/createServerEnv.mjs';
21
+ import killTaggedBrowser from '../utils/killTaggedBrowser.mjs';
22
+ import readBasePath from '../utils/readBasePath.mjs';
23
+ import resolveDevAuthUrl from '../utils/resolveDevAuthUrl.mjs';
20
24
 
21
25
  function createStdErrLineHandler({ context }) {
22
26
  const port = context.internalPort;
@@ -33,7 +37,31 @@ function createStdErrLineHandler({ context }) {
33
37
 
34
38
  function startServer(context) {
35
39
  context.shutdownServer();
36
- warnAuthUrlPortMismatch({ context });
40
+
41
+ // The app, its API and the /lowdefy-docs tools all live under basePath, and
42
+ // a config change that edits basePath restarts the child - so the URL is
43
+ // recorded on every start. Everything that finds this server through
44
+ // .lowdefy/instance.json (lowdefy mcp, the hub, lowdefy test) appends its
45
+ // paths to this URL.
46
+ context.basePath = readBasePath(context);
47
+ context.url = `http://localhost:${context.options.port}${context.basePath}`;
48
+ context.instance.update({ url: context.url });
49
+
50
+ const env = createServerEnv(context);
51
+ const configuredAuthUrl = process.env.BETTER_AUTH_URL;
52
+ const { authUrl, rewritten } = resolveDevAuthUrl({
53
+ configured: configuredAuthUrl,
54
+ port: context.options.port,
55
+ });
56
+ if (rewritten && context.loggedAuthUrl !== authUrl) {
57
+ context.logger.info(
58
+ `BETTER_AUTH_URL ${configuredAuthUrl} names another port; this dev server uses ${authUrl}.`
59
+ );
60
+ context.loggedAuthUrl = authUrl;
61
+ }
62
+
63
+ // New per child start, so killing one child's browser can match nothing else.
64
+ const browserTag = crypto.randomUUID();
37
65
 
38
66
  // The child binds context.internalPort on loopback; the manager's proxy owns
39
67
  // the public context.options.port (see startProxy.mjs) so a restart never
@@ -41,6 +69,8 @@ function startServer(context) {
41
69
  const devServer = spawn(
42
70
  'node',
43
71
  [
72
+ // For the child's idle GC (lib/server/startIdleGc.js).
73
+ '--expose-gc',
44
74
  context.bin.vite,
45
75
  '--host',
46
76
  '127.0.0.1',
@@ -49,11 +79,21 @@ function startServer(context) {
49
79
  '--strictPort',
50
80
  ],
51
81
  {
52
- stdio: ['ignore', 'inherit', 'pipe'],
82
+ // The manager holds the child's stdin and never writes to it: the pipe
83
+ // closes when the manager dies, however it dies (SIGKILL, out of
84
+ // memory), and the child exits on that (see vite.config.js).
85
+ stdio: ['pipe', 'inherit', 'pipe'],
53
86
  env: {
54
- ...process.env,
55
- LOWDEFY_DIRECTORY_CONFIG: context.directories.config,
56
- PORT: context.internalPort,
87
+ ...env,
88
+ LOWDEFY_BROWSER_TAG: browserTag,
89
+ LOWDEFY_EXIT_ON_STDIN_CLOSE: '1',
90
+ // The manager's owner and registry record cover the child: it does
91
+ // not register, and it lives and dies with the manager.
92
+ LOWDEFY_EXIT_WITH_PID: undefined,
93
+ LOWDEFY_SERVER_REGISTRY_DIR: undefined,
94
+ // Reported as the MCP serverInfo version: lowdefy mcp takes each
95
+ // tool's definition from the newest Lowdefy version it meets.
96
+ LOWDEFY_SERVER_DEV_VERSION: context.version,
57
97
  },
58
98
  }
59
99
  );
@@ -71,6 +111,7 @@ function startServer(context) {
71
111
  context.logger.debug(`Started dev server with pid ${devServer.pid}.`);
72
112
  devServer.on('exit', (code, signal) => {
73
113
  context.logger.debug(`devServer exit ${devServer.pid}, signal: ${signal}, code: ${code}`);
114
+ killTaggedBrowser({ tag: browserTag });
74
115
  });
75
116
  devServer.on('error', (error) => {
76
117
  context.logger.error(error);