@lowdefy/server-dev 6.1.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 (623) 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 +2 -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 +222 -36
  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/{getAuthConfig.js → getAuth.js} +15 -9
  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/{session.js → getStrategies.js} +17 -16
  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 +168 -41
  348. package/lib/server/createLowdefyContext.test.mjs +328 -8
  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 +15 -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/sinkExternalConnections.js +95 -0
  428. package/lib/server/sinkExternalConnections.test.mjs +135 -0
  429. package/lib/server/skipStaleMapWrites.js +62 -0
  430. package/lib/server/skipStaleMapWrites.test.mjs +113 -0
  431. package/lib/server/startIdleGc.js +67 -0
  432. package/lib/server/startIdleGc.test.mjs +138 -0
  433. package/lib/server/testFixtures/importAppCodeTwice.mjs +70 -0
  434. package/lib/server/trackSessionWork.js +30 -0
  435. package/lib/vite/createDevServerExclude.js +40 -0
  436. package/lib/vite/createDevServerExclude.test.mjs +79 -0
  437. package/manager/getContext.mjs +24 -2
  438. package/manager/processes/initialBuild.mjs +14 -0
  439. package/manager/processes/initialBuild.test.mjs +101 -0
  440. package/manager/processes/lowdefyBuild.mjs +30 -7
  441. package/manager/processes/lowdefyBuild.skipPluginChecks.test.mjs +62 -0
  442. package/manager/processes/lowdefyBuild.test.mjs +136 -1
  443. package/manager/processes/optimizeDependencies.mjs +59 -0
  444. package/manager/processes/restartServer.mjs +34 -6
  445. package/manager/processes/restartServer.test.mjs +162 -0
  446. package/manager/processes/shutdownServer.mjs +11 -0
  447. package/manager/processes/shutdownServer.test.mjs +45 -0
  448. package/manager/processes/startFirstServer.mjs +46 -0
  449. package/manager/processes/startFirstServer.test.mjs +326 -0
  450. package/manager/processes/startMailSink.mjs +135 -0
  451. package/manager/processes/startMailSink.test.mjs +146 -0
  452. package/manager/processes/startProxy.mjs +129 -12
  453. package/manager/processes/startProxy.test.mjs +366 -0
  454. package/manager/processes/startServer.mjs +47 -6
  455. package/manager/processes/startServer.test.mjs +155 -0
  456. package/manager/processes/startWatchers.mjs +6 -4
  457. package/manager/processes/syncServer.mjs +69 -0
  458. package/manager/processes/syncServer.test.mjs +141 -0
  459. package/manager/processes/waitForDevServer.mjs +48 -0
  460. package/manager/processes/waitForDevServer.test.mjs +112 -0
  461. package/manager/run.mjs +128 -43
  462. package/manager/utils/BatchChanges.mjs +21 -5
  463. package/manager/utils/BatchChanges.test.mjs +77 -0
  464. package/manager/utils/acquireDevInstance.mjs +99 -0
  465. package/manager/utils/acquireDevInstance.test.mjs +160 -0
  466. package/manager/utils/createBuildActivity.mjs +67 -0
  467. package/manager/utils/createBuildActivity.test.mjs +58 -0
  468. package/manager/utils/createChangeSignal.mjs +39 -0
  469. package/manager/utils/createChangeSignal.test.mjs +62 -0
  470. package/manager/utils/createCustomPluginMessagesMap.mjs +23 -33
  471. package/manager/utils/createCustomPluginTypesMap.mjs +24 -30
  472. package/manager/utils/createCustomPluginTypesMap.test.mjs +94 -0
  473. package/manager/utils/createDotPathIgnore.mjs +44 -0
  474. package/manager/utils/createDotPathIgnore.test.mjs +65 -0
  475. package/manager/utils/createIdleGc.mjs +51 -0
  476. package/manager/utils/createIdleGc.test.mjs +77 -0
  477. package/manager/utils/createRequestActivity.mjs +100 -0
  478. package/manager/utils/createRequestActivity.test.mjs +121 -0
  479. package/manager/utils/createServerArtifactTracker.mjs +83 -0
  480. package/manager/utils/createServerArtifactTracker.test.mjs +78 -0
  481. package/manager/utils/createServerEnv.mjs +39 -0
  482. package/manager/utils/filesHaveSameBytes.mjs +41 -0
  483. package/manager/utils/findBuildFilesOutsideWatch.mjs +59 -0
  484. package/manager/utils/findBuildFilesOutsideWatch.test.mjs +111 -0
  485. package/manager/utils/importFresh.mjs +46 -0
  486. package/manager/utils/importFresh.test.mjs +72 -0
  487. package/manager/utils/importFreshWorker.mjs +20 -0
  488. package/manager/utils/killTaggedBrowser.mjs +35 -0
  489. package/manager/utils/pagePathChanged.mjs +56 -0
  490. package/manager/utils/pruneRecordings.mjs +85 -0
  491. package/manager/utils/pruneRecordings.test.mjs +117 -0
  492. package/manager/utils/publishBuildDirectory.mjs +121 -0
  493. package/manager/utils/publishBuildDirectory.order.test.mjs +74 -0
  494. package/manager/utils/publishBuildDirectory.test.mjs +262 -0
  495. package/manager/utils/readBasePath.mjs +27 -0
  496. package/manager/utils/readBasePath.test.mjs +43 -0
  497. package/manager/utils/readBuildStatusWait.mjs +67 -0
  498. package/manager/utils/readManagerVersion.mjs +30 -0
  499. package/manager/utils/readManagerVersion.test.mjs +35 -0
  500. package/manager/utils/readPluginDefinitions.mjs +36 -0
  501. package/manager/utils/resolveDevAuthUrl.mjs +58 -0
  502. package/manager/utils/resolveDevAuthUrl.test.mjs +107 -0
  503. package/manager/utils/resolvePorts.mjs +49 -0
  504. package/manager/utils/setupWatcher.mjs +19 -11
  505. package/manager/utils/startRecordingPruner.mjs +45 -0
  506. package/manager/utils/startRecordingPruner.test.mjs +66 -0
  507. package/manager/utils/waitForChildExit.mjs +53 -0
  508. package/manager/utils/waitForChildExit.test.mjs +70 -0
  509. package/manager/utils/waitForServer.mjs +44 -0
  510. package/manager/utils/writeBuildStatus.mjs +1 -1
  511. package/manager/watchers/createPluginSourceIgnore.mjs +77 -0
  512. package/manager/watchers/envWatcher.mjs +9 -2
  513. package/manager/watchers/envWatcher.test.mjs +87 -0
  514. package/manager/watchers/listServerSidePackages.mjs +47 -0
  515. package/manager/watchers/listServerSidePackages.test.mjs +38 -0
  516. package/manager/watchers/lowdefyBuildWatcher.mjs +106 -22
  517. package/manager/watchers/lowdefyBuildWatcher.test.mjs +309 -0
  518. package/manager/watchers/pluginSourceWatcher.mjs +79 -0
  519. package/manager/watchers/pluginSourceWatcher.test.mjs +222 -0
  520. package/manager/watchers/restartRequestWatcher.mjs +71 -0
  521. package/manager/watchers/restartRequestWatcher.test.mjs +111 -0
  522. package/manager/watchers/selectLocalPluginPackages.mjs +48 -0
  523. package/manager/watchers/selectLocalPluginPackages.test.mjs +73 -0
  524. package/manager/watchers/serverPackageWatcher.mjs +38 -0
  525. package/package.json +60 -47
  526. package/package.original.json +60 -47
  527. package/src/app.js +131 -20
  528. package/src/html/renderDevPage.js +63 -11
  529. package/src/html/renderDevPage.test.mjs +138 -0
  530. package/src/lib/getRequestPath.js +29 -0
  531. package/src/middleware/createSameOriginGuard.js +37 -0
  532. package/src/middleware/createSameOriginGuard.test.mjs +102 -0
  533. package/src/middleware/errorHandler.js +51 -1
  534. package/src/middleware/errorHandler.test.mjs +173 -3
  535. package/src/middleware/isRebindingSafeHost.js +44 -0
  536. package/src/middleware/isRebindingSafeHost.test.mjs +32 -0
  537. package/src/middleware/localDevToolsOnly.js +38 -0
  538. package/src/middleware/localDevToolsOnly.test.mjs +61 -0
  539. package/src/middleware/noStore.js +27 -0
  540. package/src/middleware/noStore.test.mjs +48 -0
  541. package/src/middleware/requestTimeout.js +38 -0
  542. package/src/middleware/staleFlag.js +76 -0
  543. package/src/middleware/staleFlag.test.mjs +127 -0
  544. package/src/middleware/stampCaller.js +38 -0
  545. package/src/middleware/stampCaller.test.mjs +64 -0
  546. package/src/routes/agent.js +15 -3
  547. package/src/routes/auth.js +29 -20
  548. package/src/routes/auth.test.mjs +30 -56
  549. package/src/routes/clientError.js +13 -13
  550. package/src/routes/cron.js +3 -1
  551. package/src/routes/cronForward.js +3 -1
  552. package/src/routes/detached.js +22 -9
  553. package/src/routes/devInspect.js +17 -8
  554. package/src/routes/devInspect.test.mjs +44 -7
  555. package/src/routes/devRecording.js +113 -0
  556. package/src/routes/devRecording.test.mjs +253 -0
  557. package/src/routes/docs/buildStatus.js +10 -2
  558. package/src/routes/docs/buildStatus.test.mjs +46 -0
  559. package/src/routes/docs/content.js +5 -3
  560. package/src/routes/docs/evalOperator.js +15 -1
  561. package/src/routes/docs/events.js +50 -0
  562. package/src/routes/docs/events.test.mjs +117 -0
  563. package/src/routes/docs/find.js +9 -1
  564. package/src/routes/docs/icons.js +30 -0
  565. package/src/routes/docs/inspectState.js +16 -1
  566. package/src/routes/docs/journey.js +194 -0
  567. package/src/routes/docs/journey.test.mjs +401 -0
  568. package/src/routes/docs/journeyRefusals.test.mjs +71 -0
  569. package/src/routes/docs/loadState.js +4 -2
  570. package/src/routes/docs/mcp.js +34 -3
  571. package/src/routes/docs/mcp.test.mjs +130 -0
  572. package/src/routes/docs/mutants.js +78 -0
  573. package/src/routes/docs/mutants.test.mjs +180 -0
  574. package/src/routes/docs/parsePathParamsParam.js +41 -0
  575. package/src/routes/docs/parsePathParamsParam.test.mjs +42 -0
  576. package/src/routes/docs/parseUserParam.js +27 -21
  577. package/src/routes/docs/parseUserParam.test.mjs +17 -5
  578. package/src/routes/docs/restart.js +35 -0
  579. package/src/routes/docs/restart.test.mjs +64 -0
  580. package/src/routes/docs/runEndpoint.js +54 -0
  581. package/src/routes/docs/runEndpoint.test.mjs +165 -0
  582. package/src/routes/docs/runRequest.js +25 -8
  583. package/src/routes/docs/runRequest.test.mjs +92 -0
  584. package/src/routes/docs/screenshot.js +50 -2
  585. package/src/routes/docs/screenshot.test.mjs +117 -0
  586. package/src/routes/docs/search.js +15 -2
  587. package/src/routes/docs/snapshotState.js +17 -3
  588. package/src/routes/endpoints.js +27 -26
  589. package/src/routes/endpointsTextAnswer.test.mjs +77 -0
  590. package/src/routes/feedback.js +10 -13
  591. package/src/routes/jitPage.js +99 -35
  592. package/src/routes/jitPage.test.mjs +248 -0
  593. package/src/routes/mcp.js +82 -5
  594. package/src/routes/mcp.test.mjs +215 -0
  595. package/src/routes/mcpMemberToken.test.mjs +125 -0
  596. package/src/routes/mcpProtectedResourceMetadata.js +34 -0
  597. package/src/routes/mcpProtectedResourceMetadata.test.mjs +65 -0
  598. package/src/routes/mountDevPageRoutes.js +25 -0
  599. package/src/routes/mountDevPageRoutes.test.mjs +30 -0
  600. package/src/routes/mountOauthDiscovery.js +44 -0
  601. package/src/routes/mountOauthDiscovery.test.mjs +79 -0
  602. package/src/routes/normalizeMcpProtocolVersionHeader.js +46 -0
  603. package/src/routes/normalizeMcpProtocolVersionHeader.test.mjs +62 -0
  604. package/src/routes/reload.js +16 -0
  605. package/src/routes/request.js +6 -4
  606. package/src/routes/request.test.mjs +9 -5
  607. package/src/routes/requestBody.test.mjs +159 -0
  608. package/src/routes/usage.js +6 -2
  609. package/src/routes/user.js +27 -0
  610. package/src/routes/websocket.js +29 -8
  611. package/src/routes/websocket.test.mjs +65 -0
  612. package/src/routes/wellKnownFallback.js +30 -0
  613. package/src/routes/wellKnownFallback.test.mjs +32 -0
  614. package/src/websocket/devWebSocket.js +41 -26
  615. package/src/websocket/devWebSocket.test.mjs +157 -0
  616. package/vite.config.js +53 -23
  617. package/lib/server/auth/getDevSession.js +0 -70
  618. package/lib/server/auth/getDevSession.test.mjs +0 -138
  619. package/manager/processes/warnAuthUrlPortMismatch.mjs +0 -47
  620. package/manager/processes/warnAuthUrlPortMismatch.test.mjs +0 -93
  621. package/manager/watchers/moduleBuildWatcher.mjs +0 -79
  622. package/manager/watchers/serverArtifactWatcher.mjs +0 -120
  623. package/src/routes/apiPage.js +0 -37
package/manager/run.mjs CHANGED
@@ -15,22 +15,30 @@
15
15
  limitations under the License.
16
16
  */
17
17
 
18
- import { wait } from '@lowdefy/helpers';
19
- import { findAvailablePort } from '@lowdefy/node-utils';
20
18
  import opener from 'opener';
19
+ import { registerServer, watchOwner } from '@lowdefy/node-utils';
20
+ import checkAgentFiles from '../lib/docs/checkAgentFiles.js';
21
21
  import getContext from './getContext.mjs';
22
+ import getLowdefyVersion from './utils/getLowdefyVersion.mjs';
23
+ import acquireDevInstance from './utils/acquireDevInstance.mjs';
24
+ import createBuildActivity from './utils/createBuildActivity.mjs';
25
+ import createIdleGc from './utils/createIdleGc.mjs';
26
+ import createRequestActivity from './utils/createRequestActivity.mjs';
27
+ import startMailSink from './processes/startMailSink.mjs';
22
28
  import startProxy from './processes/startProxy.mjs';
23
- import startServer from './processes/startServer.mjs';
29
+ import startFirstServer from './processes/startFirstServer.mjs';
30
+ import startRecordingPruner from './utils/startRecordingPruner.mjs';
24
31
  import formatNoticeBox from './utils/formatNoticeBox.mjs';
32
+ import resolvePorts from './utils/resolvePorts.mjs';
25
33
 
26
34
  /*
27
35
  The run script does the following:
28
- - Run the initial Lowdefy build, install plugins, and next build and read .env
36
+ - Read .env, run the initial Lowdefy build and install plugins
29
37
  - Start file watchers to reload config and restart server if necessary
30
38
  - Start the server
31
39
  - Open a browser window.
32
40
 
33
- Three watchers are started:
41
+ Watchers (see processes/startWatchers.mjs):
34
42
 
35
43
  ## Lowdefy build watcher
36
44
  Watches:
@@ -48,25 +56,14 @@ The run script does the following:
48
56
  If the .env file is changed, the new file is parsed, and the server restarted with the new env
49
57
  and the server hard reloads.
50
58
 
51
- ## Next build watcher
59
+ ## Local plugin, restart request and server package.json watchers
52
60
 
53
- The Next build watcher watches for any files where the app should be rebuilt and restarted.
54
- It watches:
55
- - <build-dir>/plugins/**
56
- - <build-dir>/config.json
57
- - <server-dir>/package.json
61
+ A local plugin change rebuilds (and restarts when the plugin has server-side types), a restart
62
+ requested by the dev tools rebuilds and restarts, and a plugin package a page build added to
63
+ the server's package.json is installed.
58
64
 
59
- If app config changes:
60
- - <build-dir>/config.json changes, rebuild and restart server.
61
-
62
- If user styles change:
63
- - <public-dir>/styles.css changes, rebuild and restart server.
64
-
65
- If new plugin type in an existing plugin package is used:
66
- - <build-dir>/plugins/** changes, rebuild next and restart server.
67
-
68
- If new plugin type in a new plugin package is used:
69
- - <server-dir>/package.json changes, run npm install, rebuild next and restart server.
65
+ After every build, syncServer restarts the server when a file it read at start changed (config,
66
+ auth, connections, server operators), installing new plugin packages first.
70
67
 
71
68
  # Reload mechanism
72
69
 
@@ -81,45 +78,123 @@ The run script does the following:
81
78
 
82
79
  const context = await getContext();
83
80
 
81
+ const instance = acquireDevInstance({
82
+ configDirectory: context.directories.config,
83
+ owner: context.options.owner,
84
+ version: context.version,
85
+ });
86
+ if (instance.acquired === false) {
87
+ const { holder } = instance;
88
+ const where = holder.port ? `, http://localhost:${holder.port}` : '';
89
+ context.logger.error(
90
+ `A dev server for this app is already running (${holder.owner ?? 'terminal'}${where}, pid ${
91
+ holder.pid
92
+ }). ` +
93
+ 'Two dev servers race writing the build directory. Use the running one, or stop it first.'
94
+ );
95
+ process.exit(1);
96
+ }
97
+ process.on('exit', () => instance.release());
98
+ startRecordingPruner({ configDirectory: context.directories.config, logger: context.logger });
99
+
100
+ // Requests through the proxy and finished builds are use of the server; the
101
+ // hub stops a server nobody has used for its idle limit (see
102
+ // createRequestActivity).
103
+ context.requestActivity = createRequestActivity({
104
+ onChange: (fields) => instance.update(fields),
105
+ });
106
+
107
+ // `building` is true while a change is queued or being processed, restarts
108
+ // included. lowdefy_build_status({ wait: true }) waits on it, so an agent
109
+ // reads the build that includes its last edit instead of the one before.
110
+ // One full GC shortly after each build, which is when the manager's heap grows.
111
+ const idleGc = createIdleGc();
112
+
113
+ context.buildActivity = createBuildActivity({
114
+ onChange: (building) => {
115
+ instance.update({ building });
116
+ idleGc.onBuildingChange(building);
117
+ if (!building) {
118
+ context.requestActivity.touch();
119
+ }
120
+ },
121
+ });
122
+
123
+ // Machine-wide record of this manager and its owner, for `lowdefy hub ps|prune`.
124
+ // Written only when the CLI sets the registry directory.
125
+ const registration = registerServer({
126
+ kind: 'dev',
127
+ port: context.options.port,
128
+ configDirectory: context.directories.config,
129
+ logger: context.logger,
130
+ });
131
+
132
+ function shutdown() {
133
+ context.shutdownServer();
134
+ process.exit(0);
135
+ }
136
+
84
137
  // Shut the Vite child down on direct signals (process managers, scripts/dev.mjs
85
138
  // signal forwarding) — terminal Ctrl+C signals the whole process group, but a
86
139
  // targeted SIGTERM would otherwise orphan the child.
87
- for (const signal of ['SIGINT', 'SIGTERM']) {
88
- process.on(signal, () => {
89
- context.shutdownServer();
90
- process.exit(0);
91
- });
140
+ for (const signal of ['SIGINT', 'SIGTERM', 'SIGHUP']) {
141
+ process.on(signal, shutdown);
92
142
  }
93
143
 
144
+ // A manager started by the CLI, a test runner or a script stops when that
145
+ // owner stops, however it ends. Inert unless the spawner set its variables.
146
+ watchOwner({
147
+ onExit: ({ reason }) => {
148
+ context.logger.info(`The process that started this dev server is gone (${reason}).`);
149
+ shutdown();
150
+ },
151
+ });
152
+
94
153
  try {
95
154
  await context.initialBuild();
96
155
 
97
- // We are not waiting for the startWatchers promise to resolve (all watchers have fired the ready event)
98
- // because chokidar sometimes doesn't fire this event, and it seems like there isn't an issue with not waiting.
156
+ // Not awaited: chokidar's ready event is unreliable. Started now so an edit
157
+ // made during the rest of the start-up is seen; a batch that syncs before
158
+ // the first child start waits for it (see syncServer).
99
159
  context.startWatchers();
100
160
 
101
- // The manager is the component that binds the port, so it re-checks here to
161
+ // The manager is the component that binds the port, so it checks here to
102
162
  // cover every launch path (CLI, monorepo dev script, direct run.mjs) and any
103
163
  // process that grabbed the port during the initial build.
104
- const availablePort = await findAvailablePort({ port: context.options.port });
105
- if (availablePort !== context.options.port) {
106
- context.logger.warn(
107
- `Port ${context.options.port} is in use, using port ${availablePort} instead.`
108
- );
109
- context.options.port = availablePort;
110
- }
164
+ const { port, internalPort } = await resolvePorts(context);
165
+ context.options.port = port;
166
+ context.internalPort = internalPort;
167
+ instance.update({ port, internalPort });
168
+ registration?.update({ port });
169
+ context.instance = instance;
170
+
171
+ // Called by waitForDevServer whenever a child answers: the first start's,
172
+ // one that answers after its wait, or a restart's after a first child that
173
+ // exited. The hub and the MCP shim wait on ready.
174
+ let ready = false;
175
+ context.markServerReady = () => {
176
+ if (ready) {
177
+ return;
178
+ }
179
+ ready = true;
180
+ // A slow first build must not make a fresh server look idle the moment it is ready.
181
+ context.requestActivity.touch();
182
+ instance.update({ state: 'ready' });
183
+ };
111
184
 
112
185
  // The manager holds the public port for the whole session and proxies to the
113
186
  // Vite child on an internal loopback port — restarting the child (js module
114
187
  // or .env change) then never drops the listener, so long-lived clients (MCP
115
188
  // agents, the reload SSE stream, HMR websockets) reconnect instead of dying
116
189
  // on ECONNREFUSED.
117
- context.internalPort = await findAvailablePort({ port: context.options.port + 1 });
118
190
  await startProxy(context);
191
+ context.mailSink = await startMailSink(context);
119
192
 
120
- startServer(context);
121
- await wait(800);
122
- const docsUrl = `http://localhost:${context.options.port}/lowdefy-docs`;
193
+ // Optimises dependencies, starts the child and waits for it to answer.
194
+ if (!(await startFirstServer(context))) {
195
+ context.logger.warn('The dev server did not answer within 2 minutes - check the output above.');
196
+ }
197
+ const docsUrl = `${context.url}/lowdefy-docs`;
123
198
  context.logger.info(
124
199
  { color: 'blue' },
125
200
  formatNoticeBox({
@@ -137,9 +212,19 @@ try {
137
212
  ],
138
213
  })
139
214
  );
140
- if (process.env.LOWDEFY_SERVER_DEV_OPEN_BROWSER === 'true') {
215
+ // Lowdefy's agent block: missing, from another version, or not imported by
216
+ // CLAUDE.md. Build status reports the same notices to agents on every read.
217
+ const agentFiles = checkAgentFiles({
218
+ configDirectory: context.directories.config,
219
+ lowdefyVersion: await getLowdefyVersion(context).catch(() => null),
220
+ });
221
+ for (const notice of agentFiles) {
222
+ context.logger.warn(notice.message);
223
+ }
224
+ // A hub-owned server was started for an agent, not for a person at a browser.
225
+ if (process.env.LOWDEFY_SERVER_DEV_OPEN_BROWSER === 'true' && context.options.owner !== 'hub') {
141
226
  // TODO: Wait 1 sec for a ping and don't open if a ping is seen
142
- opener(`http://localhost:${context.options.port}`);
227
+ opener(context.url);
143
228
  }
144
229
  await new Promise(() => {});
145
230
  } catch (error) {
@@ -17,21 +17,40 @@
17
17
  import { type } from '@lowdefy/helpers';
18
18
 
19
19
  class BatchChanges {
20
- constructor({ context, fn, delay }) {
20
+ constructor({ context, fn, delay, onBusy }) {
21
21
  this.context = context;
22
22
  this._call = this._call.bind(this);
23
23
  this.args = [];
24
+ this.busy = false;
24
25
  this.delay = delay || 500;
25
26
  this.fn = fn;
27
+ this.onBusy = onBusy ?? (() => {});
26
28
  this.repeat = false;
27
29
  this.running = false;
28
30
  }
29
31
 
30
32
  newChange(...args) {
31
33
  this.args.push(args.filter((arg) => type.isString(arg))); // filter for string paths since chokidar also returns an stats object on windows.
34
+ // Busy from the first change of a batch until the batch - and any batch
35
+ // queued behind it - has been processed, so a reader can wait for its
36
+ // edits to land instead of racing the debounce.
37
+ if (!this.busy) {
38
+ this.busy = true;
39
+ this.onBusy(true);
40
+ }
32
41
  this._startTimer();
33
42
  }
34
43
 
44
+ _settle() {
45
+ if (this.repeat) {
46
+ this.repeat = false;
47
+ this._call();
48
+ return;
49
+ }
50
+ this.busy = false;
51
+ this.onBusy(false);
52
+ }
53
+
35
54
  _startTimer() {
36
55
  if (this.timer) {
37
56
  clearTimeout(this.timer);
@@ -50,14 +69,11 @@ class BatchChanges {
50
69
  this.args = [];
51
70
  await this.fn(args);
52
71
  this.running = false;
53
- if (this.repeat) {
54
- this.repeat = false;
55
- this._call();
56
- }
57
72
  } catch (error) {
58
73
  this.running = false;
59
74
  this.context.logger.error(error?.message ?? error);
60
75
  }
76
+ this._settle();
61
77
  }
62
78
  }
63
79
 
@@ -0,0 +1,77 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */
16
+
17
+ import { jest } from '@jest/globals';
18
+ import { wait } from '@lowdefy/helpers';
19
+
20
+ import BatchChanges from './BatchChanges.mjs';
21
+
22
+ const context = { logger: { error: jest.fn() } };
23
+
24
+ test('BatchChanges reports busy from the first change until the batch is processed', async () => {
25
+ const events = [];
26
+ const fn = jest.fn(async () => {
27
+ events.push('processed');
28
+ });
29
+ const batch = new BatchChanges({
30
+ context,
31
+ fn,
32
+ delay: 20,
33
+ onBusy: (busy) => events.push(busy ? 'busy' : 'idle'),
34
+ });
35
+ batch.newChange('a.yaml');
36
+ batch.newChange('b.yaml');
37
+ expect(events).toEqual(['busy']);
38
+ await wait(80);
39
+ expect(events).toEqual(['busy', 'processed', 'idle']);
40
+ expect(fn).toHaveBeenCalledWith([['a.yaml'], ['b.yaml']]);
41
+ });
42
+
43
+ test('BatchChanges stays busy through a change that arrives while a batch is running', async () => {
44
+ const events = [];
45
+ let batch;
46
+ const fn = jest.fn(async (args) => {
47
+ events.push(`processed ${args.flat().join(',')}`);
48
+ if (args.flat().includes('a.yaml')) {
49
+ batch.newChange('b.yaml');
50
+ await wait(10);
51
+ }
52
+ });
53
+ batch = new BatchChanges({
54
+ context,
55
+ fn,
56
+ delay: 10,
57
+ onBusy: (busy) => events.push(busy ? 'busy' : 'idle'),
58
+ });
59
+ batch.newChange('a.yaml');
60
+ await wait(100);
61
+ expect(events).toEqual(['busy', 'processed a.yaml', 'processed b.yaml', 'idle']);
62
+ });
63
+
64
+ test('BatchChanges clears busy when processing throws', async () => {
65
+ const events = [];
66
+ const batch = new BatchChanges({
67
+ context,
68
+ fn: async () => {
69
+ throw new Error('build failed');
70
+ },
71
+ delay: 10,
72
+ onBusy: (busy) => events.push(busy ? 'busy' : 'idle'),
73
+ });
74
+ batch.newChange('a.yaml');
75
+ await wait(50);
76
+ expect(events).toEqual(['busy', 'idle']);
77
+ });
@@ -0,0 +1,99 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */
16
+
17
+ import fs from 'fs';
18
+ import path from 'path';
19
+
20
+ import { getDevInstancePath, getProcessStartTime, readDevInstance } from '@lowdefy/node-utils';
21
+
22
+ function writeRecord({ instancePath, record }) {
23
+ // Written whole and renamed into place so readers never see half a record.
24
+ const temporaryPath = `${instancePath}.${process.pid}.tmp`;
25
+ fs.mkdirSync(path.dirname(instancePath), { recursive: true });
26
+ fs.writeFileSync(temporaryPath, `${JSON.stringify(record, null, 2)}\n`, { mode: 0o600 });
27
+ fs.renameSync(temporaryPath, instancePath);
28
+ }
29
+
30
+ // One dev server per app. Two managers on one app race each other's builds -
31
+ // one wedges on a half-deleted pages directory and keeps serving a stale build
32
+ // with no error anywhere - so the second refuses. The record doubles as the
33
+ // discovery file: the CLI, the MCP shim and the hub read it to find this
34
+ // server's port, owner and readiness without a registry of their own.
35
+ // Creating the file exclusively decides a race between two managers starting
36
+ // at once: exactly one create succeeds.
37
+ function createRecord({ instancePath, record }) {
38
+ fs.mkdirSync(path.dirname(instancePath), { recursive: true });
39
+ try {
40
+ const fd = fs.openSync(instancePath, 'wx', 0o600);
41
+ fs.writeSync(fd, `${JSON.stringify(record, null, 2)}\n`);
42
+ fs.closeSync(fd);
43
+ return true;
44
+ } catch (error) {
45
+ if (error.code === 'EEXIST') {
46
+ return false;
47
+ }
48
+ throw error;
49
+ }
50
+ }
51
+
52
+ function acquireDevInstance({ configDirectory, owner, version }) {
53
+ const instancePath = getDevInstancePath({ configDirectory });
54
+ let record = {
55
+ pid: process.pid,
56
+ // Readers check it against the pid's process, so a record left by a
57
+ // killed manager never passes for a live one on a reused pid.
58
+ processStartTime: getProcessStartTime({ pid: process.pid }),
59
+ // The path as stored on disk, whatever case the terminal's working
60
+ // directory was typed in (macOS), so lowdefy mcp and the hub match it.
61
+ configDirectory: fs.realpathSync.native(configDirectory),
62
+ owner,
63
+ state: 'starting',
64
+ building: false,
65
+ version,
66
+ startedAt: new Date().toISOString(),
67
+ };
68
+ if (!createRecord({ instancePath, record })) {
69
+ const holder = readDevInstance({ configDirectory });
70
+ if (holder !== null && holder.pid !== process.pid) {
71
+ return { acquired: false, holder, instancePath };
72
+ }
73
+ // Stale: the holder exited, or the file was copied from another checkout.
74
+ fs.rmSync(instancePath, { force: true });
75
+ if (!createRecord({ instancePath, record })) {
76
+ return { acquired: false, holder: readDevInstance({ configDirectory }) ?? {}, instancePath };
77
+ }
78
+ }
79
+
80
+ function update(fields) {
81
+ record = { ...record, ...fields };
82
+ writeRecord({ instancePath, record });
83
+ }
84
+
85
+ function release() {
86
+ try {
87
+ const current = JSON.parse(fs.readFileSync(instancePath, 'utf8'));
88
+ if (current.pid === process.pid) {
89
+ fs.rmSync(instancePath, { force: true });
90
+ }
91
+ } catch {
92
+ // Already gone, or taken over after a crash - nothing to release.
93
+ }
94
+ }
95
+
96
+ return { acquired: true, instancePath, release, update };
97
+ }
98
+
99
+ export default acquireDevInstance;
@@ -0,0 +1,160 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */
16
+
17
+ import fs from 'fs';
18
+ import os from 'os';
19
+ import path from 'path';
20
+ import { spawnSync } from 'child_process';
21
+
22
+ import { getProcessStartTime, isProcessStartTime, readDevInstance } from '@lowdefy/node-utils';
23
+
24
+ import acquireDevInstance from './acquireDevInstance.mjs';
25
+
26
+ let configDirectory;
27
+
28
+ beforeEach(() => {
29
+ configDirectory = fs.realpathSync.native(fs.mkdtempSync(path.join(os.tmpdir(), 'dev-instance-')));
30
+ });
31
+
32
+ afterEach(() => {
33
+ fs.rmSync(configDirectory, { recursive: true, force: true });
34
+ });
35
+
36
+ function instancePath() {
37
+ return path.join(configDirectory, '.lowdefy', 'instance.json');
38
+ }
39
+
40
+ function readRecord() {
41
+ return JSON.parse(fs.readFileSync(instancePath(), 'utf8'));
42
+ }
43
+
44
+ // Windows has no owner-only file mode bits.
45
+ const onPosix = process.platform === 'win32' ? test.skip : test;
46
+
47
+ test('acquireDevInstance writes a starting record for this process', () => {
48
+ const instance = acquireDevInstance({ configDirectory, owner: 'terminal', version: '6.0.0' });
49
+ expect(instance.acquired).toBe(true);
50
+ expect(readRecord()).toMatchObject({
51
+ pid: process.pid,
52
+ configDirectory,
53
+ owner: 'terminal',
54
+ state: 'starting',
55
+ version: '6.0.0',
56
+ });
57
+ });
58
+
59
+ onPosix('acquireDevInstance writes the record readable only by the owner', () => {
60
+ acquireDevInstance({ configDirectory, owner: 'terminal', version: '6.0.0' });
61
+ expect(fs.statSync(instancePath()).mode & 0o777).toBe(0o600);
62
+ });
63
+
64
+ test('acquireDevInstance update merges fields into the record', () => {
65
+ const instance = acquireDevInstance({ configDirectory, owner: 'hub', version: '6.0.0' });
66
+ instance.update({ port: 4100, internalPort: 4101 });
67
+ instance.update({ state: 'ready' });
68
+ expect(readRecord()).toMatchObject({
69
+ owner: 'hub',
70
+ port: 4100,
71
+ internalPort: 4101,
72
+ state: 'ready',
73
+ });
74
+ });
75
+
76
+ test('acquireDevInstance release removes the record', () => {
77
+ const instance = acquireDevInstance({ configDirectory, owner: 'terminal', version: '6.0.0' });
78
+ instance.release();
79
+ expect(fs.existsSync(instancePath())).toBe(false);
80
+ });
81
+
82
+ test('acquireDevInstance refuses when a live process holds this app', () => {
83
+ // The parent process is alive for the whole test and is never this process,
84
+ // on every platform (Windows has no pid 1).
85
+ const livePid = process.ppid;
86
+ fs.mkdirSync(path.dirname(instancePath()), { recursive: true });
87
+ fs.writeFileSync(instancePath(), JSON.stringify({ pid: livePid, configDirectory, port: 3000 }));
88
+ const instance = acquireDevInstance({ configDirectory, owner: 'terminal', version: '6.0.0' });
89
+ expect(instance.acquired).toBe(false);
90
+ expect(instance.holder).toEqual({ pid: livePid, configDirectory, port: 3000 });
91
+ });
92
+
93
+ test('acquireDevInstance takes over a record whose process has exited', () => {
94
+ const dead = spawnSync(process.execPath, ['-e', '']).pid;
95
+ fs.mkdirSync(path.dirname(instancePath()), { recursive: true });
96
+ fs.writeFileSync(instancePath(), JSON.stringify({ pid: dead, configDirectory }));
97
+ const instance = acquireDevInstance({ configDirectory, owner: 'terminal', version: '6.0.0' });
98
+ expect(instance.acquired).toBe(true);
99
+ expect(readRecord().pid).toBe(process.pid);
100
+ });
101
+
102
+ test('acquireDevInstance takes over a record copied from another checkout', () => {
103
+ fs.mkdirSync(path.dirname(instancePath()), { recursive: true });
104
+ fs.writeFileSync(instancePath(), JSON.stringify({ pid: 1, configDirectory: '/other/checkout' }));
105
+ const instance = acquireDevInstance({ configDirectory, owner: 'terminal', version: '6.0.0' });
106
+ expect(instance.acquired).toBe(true);
107
+ });
108
+
109
+ test('acquireDevInstance release leaves a record taken over by a newer manager', () => {
110
+ const instance = acquireDevInstance({ configDirectory, owner: 'terminal', version: '6.0.0' });
111
+ fs.writeFileSync(instancePath(), JSON.stringify({ pid: 1, configDirectory }));
112
+ instance.release();
113
+ expect(fs.existsSync(instancePath())).toBe(true);
114
+ });
115
+
116
+ // A start time in the form this platform reads, but not the one the pid's process has: on
117
+ // Linux, the same ticks in another boot.
118
+ function otherStartTime(pid) {
119
+ const startTime = getProcessStartTime({ pid });
120
+ if (typeof startTime === 'string') {
121
+ return startTime.replace(/^linux:[^:]+:/, 'linux:00000000-0000-0000-0000-000000000000:');
122
+ }
123
+ return 0;
124
+ }
125
+
126
+ test('acquireDevInstance takes over a record left on a pid another process now has', () => {
127
+ // pid 1 is alive, but it is not the manager that wrote this record.
128
+ fs.mkdirSync(path.dirname(instancePath()), { recursive: true });
129
+ fs.writeFileSync(
130
+ instancePath(),
131
+ JSON.stringify({ pid: 1, processStartTime: otherStartTime(1), configDirectory })
132
+ );
133
+ const instance = acquireDevInstance({ configDirectory, owner: 'terminal', version: '6.0.0' });
134
+ expect(instance.acquired).toBe(true);
135
+ expect(isProcessStartTime(readRecord().processStartTime)).toBe(true);
136
+ });
137
+
138
+ // Case variants name one directory only where the file system ignores case.
139
+ const tmpReal = fs.realpathSync.native(os.tmpdir());
140
+ const onCaseInsensitiveFs =
141
+ tmpReal !== tmpReal.toUpperCase() && fs.existsSync(tmpReal.toUpperCase()) ? test : test.skip;
142
+
143
+ onCaseInsensitiveFs(
144
+ 'acquireDevInstance records the path as stored on disk when started from a case variant of it',
145
+ () => {
146
+ const variant = configDirectory.toUpperCase();
147
+ const instance = acquireDevInstance({
148
+ configDirectory: variant,
149
+ owner: 'terminal',
150
+ version: '6.0.0',
151
+ });
152
+ try {
153
+ expect(readRecord().configDirectory).toEqual(configDirectory);
154
+ expect(readDevInstance({ configDirectory })).toMatchObject({ pid: process.pid });
155
+ expect(readDevInstance({ configDirectory: variant })).toMatchObject({ pid: process.pid });
156
+ } finally {
157
+ instance.release();
158
+ }
159
+ }
160
+ );